1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-25 09:18:58 +02:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Laurent Cozic
817931f4cf fixed tests 2020-11-19 20:54:52 +00:00
Laurent Cozic
3eda7d1c26 electron 10 update 2020-11-19 18:48:17 +00:00
Laurent Cozic
592b9d95c6 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-11-19 18:35:56 +00:00
Laurent Cozic
4abdab5cdb Tools: Fixed tests 2020-11-19 18:34:53 +00:00
Caleb John
858508bfa9 Desktop: Add option to toggle spellchecking for the markdown editor (#4109) 2020-11-19 17:14:44 +00:00
Caleb John
116413e78d Desktop: Prevent lines from shifting in Markdown Editor when Scrollbar appears (#4110) 2020-11-19 17:04:18 +00:00
Anshuman Pandey
031a26116c Desktop: Resolves #4096: Disabled the auto update option in linux (#4102) 2020-11-19 17:00:22 +00:00
Laurent Cozic
65962e26ce Desktop: Fixes #4098: Fix crash due to React when trying to upgrade sync target 2020-11-19 16:38:44 +00:00
Laurent Cozic
30913a5d58 API: Always include 'has_more' field for paginated data 2020-11-19 16:25:32 +00:00
Laurent Cozic
61618fb37c All: Refresh sidebar and notes when moving note outside of conflict folder 2020-11-19 16:21:24 +00:00
Laurent Cozic
a40ab434ca Generator: Merge package.json when updating plugin framework 2020-11-19 15:57:47 +00:00
Laurent Cozic
dbbd930f81 All: Added global logger
Avoids having to pass around a logger to every module and sub-module,
and makes it easier to set log level globally.
2020-11-19 15:25:02 +00:00
Laurent Cozic
ff3ae3860e Doc Mentioned gettext version 2020-11-19 13:03:40 +00:00
Laurent Cozic
e1c4d1a417 Doc: Fix link 2020-11-19 13:02:03 +00:00
Laurent Cozic
e1180a1d84 Tools: Remove Jasmine dependency 2020-11-19 12:44:20 +00:00
Laurent Cozic
e57444dc32 Desktop: Add config screen to add, remove or enable, disable plugins 2020-11-19 12:34:49 +00:00
60 changed files with 2633 additions and 1298 deletions

View File

@@ -241,6 +241,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map
packages/app-cli/tests/support/plugins/events/src/index.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js
packages/app-cli/tests/support/plugins/events/src/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/index.js
packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/types.js
packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map
packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/src/index.js
packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map
packages/app-cli/tests/support/plugins/json_export/api/index.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map
@@ -367,6 +376,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map
packages/app-desktop/gui/ConfigScreen/SideBar.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@@ -730,6 +742,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map
packages/app-desktop/gui/ToolbarButton/styles/index.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map
packages/app-desktop/gui/dialogs.d.ts
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/dialogs.js.map
packages/app-desktop/gui/hooks/useEffectDebugger.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map
@@ -742,6 +757,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map
packages/app-desktop/gui/hooks/usePropsDebugger.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map

View File

@@ -15,7 +15,8 @@ module.exports = {
'Atomics': 'readonly',
'SharedArrayBuffer': 'readonly',
// Jasmine variables
// Jest variables
'test': 'readonly',
'expect': 'readonly',
'describe': 'readonly',
'it': 'readonly',
@@ -23,10 +24,6 @@ module.exports = {
'afterAll': 'readonly',
'beforeEach': 'readonly',
'afterEach': 'readonly',
'jasmine': 'readonly',
// Jest variables
'test': 'readonly',
// React Native variables
'__DEV__': 'readonly',

18
.gitignore vendored
View File

@@ -233,6 +233,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map
packages/app-cli/tests/support/plugins/events/src/index.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js
packages/app-cli/tests/support/plugins/events/src/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/index.js
packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/types.js
packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map
packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/src/index.js
packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map
packages/app-cli/tests/support/plugins/json_export/api/index.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map
@@ -359,6 +368,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map
packages/app-desktop/gui/ConfigScreen/SideBar.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@@ -722,6 +734,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map
packages/app-desktop/gui/ToolbarButton/styles/index.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map
packages/app-desktop/gui/dialogs.d.ts
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/dialogs.js.map
packages/app-desktop/gui/hooks/useEffectDebugger.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map
@@ -734,6 +749,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map
packages/app-desktop/gui/hooks/usePropsDebugger.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map

View File

@@ -13,6 +13,7 @@
"_vieux/": true,
".gitignore": true,
".eslintignore": true,
"**/*.jpl": true,
"./packages/app-cli/**/*.*~": true,
"./packages/app-cli/**/*.mo": true,
"./packages/app-cli/**/build/": true,
@@ -55,7 +56,6 @@
"./packages/app-desktop/**/*.min.js": true,
"./packages/app-desktop/**/dist/": true,
"./packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"./packages/app-desktop/**/lib/": true,
"./packages/app-desktop/**/node_modules/": true,
"./packages/app-desktop/**/packageInfo.js": true,
"./packages/app-desktop/**/pluginAssets/": true,
@@ -242,7 +242,6 @@
"packages/app-desktop/**/*.min.js": true,
"packages/app-desktop/**/dist/": true,
"packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"packages/app-desktop/**/lib/": true,
"packages/app-desktop/**/node_modules/": true,
"packages/app-desktop/**/packageInfo.js": true,
"packages/app-desktop/**/pluginAssets/": true,

22
package-lock.json generated
View File

@@ -1227,12 +1227,6 @@
"@types/node": "*"
}
},
"@types/jasmine": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.0.tgz",
"integrity": "sha512-CPT4r0a63e5wpNj5ejMnconM7a+0Hdx6/APsyw8AQOHk0/Mxp3xYrym1ZabWJiYuQkgKB3MonYoN04mxtvAvRA==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
@@ -6439,22 +6433,6 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
"dev": true
},
"jasmine": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.6.3.tgz",
"integrity": "sha512-Th91zHsbsALWjDUIiU5d/W5zaYQsZFMPTdeNmi8GivZPmAaUAK8MblSG3yQI4VMGC/abF2us7ex60NH1AAIMTA==",
"dev": true,
"requires": {
"glob": "^7.1.6",
"jasmine-core": "~3.6.0"
}
},
"jasmine-core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz",
"integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -7,6 +7,12 @@
},
"license": "MIT",
"scripts": {
"addPackageCli": "lerna add --scope joplin",
"addPackageCliD": "lerna add --scope joplin -D",
"addPackageDesktop": "lerna add --scope @joplin/app-desktop",
"addPackageDesktopD": "lerna add --scope @joplin/app-desktop -D",
"addPackageMobile": "lerna add --scope @joplin/app-mobile",
"addPackageMobileD": "lerna add --scope @joplin/app-mobile -D",
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
"buildDoc": "./packages/tools/build-all.sh",
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out docs/api/references/plugin_api packages/lib/services/plugins/api/",
@@ -26,11 +32,11 @@
"releaseDesktop": "node packages/tools/release-electron.js",
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
"setupNewRelease": "node ./packages/tools/setupNewRelease",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"tsc": "lerna run tsc --stream --parallel",
"test": "lerna run test --stream",
"test-ci": "lerna run test-ci --stream",
"test": "lerna run test --stream",
"tsc": "lerna run tsc --stream --parallel",
"updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "lerna run watch --stream --parallel"
},
"husky": {
@@ -39,7 +45,6 @@
}
},
"devDependencies": {
"@types/jasmine": "^3.6.0",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"eslint": "^7.6.0",
@@ -50,7 +55,6 @@
"glob": "^7.1.6",
"gulp": "^4.0.2",
"husky": "^3.0.2",
"jasmine": "^3.5.0",
"lerna": "^3.22.1",
"lint-staged": "^9.2.1",
"typedoc": "^0.17.8",

View File

@@ -426,7 +426,7 @@ class Application extends BaseApplication {
const AppGui = require('./app-gui.js');
this.gui_ = new AppGui(this, this.store(), keymap);
this.gui_.setLogger(this.logger_);
this.gui_.setLogger(this.logger());
await this.gui_.start();
// Since the settings need to be loaded before the store is created, it will never

View File

@@ -35,7 +35,7 @@ module.exports = {
'/build/',
'test-utils.js',
'file_api_driver.js',
'/tests\\/tmp/',
'<rootDir>/tests/tmp/',
],
// To avoid this warning:

View File

@@ -898,11 +898,15 @@
"@types/istanbul-lib-report": "*"
}
},
"@types/jasmine": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.0.tgz",
"integrity": "sha512-CPT4r0a63e5wpNj5ejMnconM7a+0Hdx6/APsyw8AQOHk0/Mxp3xYrym1ZabWJiYuQkgKB3MonYoN04mxtvAvRA==",
"dev": true
"@types/jest": {
"version": "26.0.15",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz",
"integrity": "sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog==",
"dev": true,
"requires": {
"jest-diff": "^26.0.0",
"pretty-format": "^26.0.0"
}
},
"@types/node": {
"version": "14.14.6",
@@ -4511,38 +4515,6 @@
"istanbul-lib-report": "^3.0.0"
}
},
"jasmine": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz",
"integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==",
"dev": true,
"requires": {
"glob": "^7.1.4",
"jasmine-core": "~3.5.0"
},
"dependencies": {
"glob": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"jasmine-core": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
"dev": true
},
"jest": {
"version": "26.6.3",
"resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz",

View File

@@ -66,10 +66,9 @@
},
"devDependencies": {
"@joplin/tools": "^1.0.9",
"@types/jasmine": "^3.6.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"gulp": "^4.0.2",
"jasmine": "^3.5.0",
"jest": "^26.6.3",
"temp": "^0.9.1",
"typescript": "^4.0.5"

View File

@@ -262,14 +262,14 @@ describe('models_Note', function() {
for (const testCase of testCases) {
const [useAbsolutePaths, input, expected] = testCase;
const internalToExternal = await Note.replaceResourceInternalToExternalLinks(input, { useAbsolutePaths });
expect(internalToExternal).toBe(expected, 'replaceResourceInternalToExternalLinks failed');
expect(internalToExternal).toBe(expected);
const externalToInternal = await Note.replaceResourceExternalToInternalLinks(internalToExternal, { useAbsolutePaths });
expect(externalToInternal).toBe(input, 'replaceResourceExternalToInternalLinks failed');
expect(externalToInternal).toBe(input);
}
const result = await Note.replaceResourceExternalToInternalLinks(`[](joplin://${note1.id})`);
expect(result).toBe(`[](:/${note1.id})`, 'replaceResourceExternalToInternalLinks failed (note link)');
expect(result).toBe(`[](:/${note1.id})`);
}));
});

View File

@@ -28,7 +28,7 @@ async function recreateExportDir() {
function fieldsEqual(model1: any, model2: any, fieldNames: string[]) {
for (let i = 0; i < fieldNames.length; i++) {
const f = fieldNames[i];
expect(model1[f]).toBe(model2[f], `For key ${f}`);
expect(model1[f]).toBe(model2[f]);
}
}

View File

@@ -4,7 +4,7 @@ import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
import MdToHtml from '@joplin/renderer/MdToHtml';
import shim from '@joplin/lib/shim';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const { asyncTest, expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder');
@@ -43,7 +43,7 @@ describe('services_PluginService', function() {
it('should load and run a simple plugin', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple`]);
await service.loadAndRunPlugins([`${testPluginDir}/simple`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
@@ -59,13 +59,13 @@ describe('services_PluginService', function() {
it('should load and run a simple plugin and handle trailing slash', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple/`]);
await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
}));
it('should load and run a plugin that uses external packages', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`]);
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {});
expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError();
const allFolders = await Folder.all();
@@ -78,7 +78,7 @@ describe('services_PluginService', function() {
it('should load multiple plugins from a directory', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`);
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {});
const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1');
const plugin2 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo2');
@@ -125,14 +125,14 @@ describe('services_PluginService', function() {
it('should load plugins from JS bundle files', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`);
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {});
expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
it('should load plugins from JPL archive', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`]);
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], {});
expect(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
@@ -248,9 +248,15 @@ describe('services_PluginService', function() {
];
for (const testCase of testCases) {
const [appVersion, expected] = testCase;
const plugin = await newPluginService(appVersion as string).loadPluginFromJsBundle('', pluginScript);
expect(plugin.enabled).toBe(expected as boolean);
const [appVersion, hasNoError] = testCase;
const service = newPluginService(appVersion as string);
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
if (hasNoError) {
await expectNotThrow(() => service.runPlugin(plugin));
} else {
await expectThrow(() => service.runPlugin(plugin));
}
}
}));

View File

@@ -3,7 +3,7 @@
const time = require('@joplin/lib/time').default;
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, mockDate, restoreDate } = require('./test-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, restoreDate } = require('./test-utils.js');
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine');
const Note = require('@joplin/lib/models/Note');
const ItemChange = require('@joplin/lib/models/ItemChange');

View File

@@ -23,12 +23,6 @@ let engine = null;
const ids = (array) => array.map(a => a.id);
// For pretty printing.
// See https://stackoverflow.com/questions/23676459/karma-jasmine-pretty-printing-object-comparison/26324116
// jasmine.pp = function(obj) {
// return JSON.stringify(obj, undefined, 2);
// };
describe('services_SearchFilter', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);

View File

@@ -538,7 +538,7 @@ describe('services_rest_Api', function() {
const r3 = await api.route(RequestMethod.GET, 'folders', { ...baseQuery, page: 3 });
expect(r3.items.length).toBe(0);
expect(r3.has_more).toBe(undefined);
expect(r3.has_more).toBe(false);
}
{
@@ -560,7 +560,7 @@ describe('services_rest_Api', function() {
expect(r2.items.length).toBe(1);
expect(r2.items[0].title).toBe('folder4');
expect(r2.has_more).toBe(undefined);
expect(r2.has_more).toBe(false);
}
}));

View File

@@ -131,9 +131,11 @@ setSyncTargetName('memory');
const syncDir = `${__dirname}/../tests/sync`;
let defaultJasmineTimeout = 90 * 1000;
if (isNetworkSyncTarget_) defaultJasmineTimeout = 60 * 1000 * 10;
if (typeof jasmine !== 'undefined') jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultJasmineTimeout;
// TODO: Should probably update this for Jest?
// let defaultJasmineTimeout = 90 * 1000;
// if (isNetworkSyncTarget_) defaultJasmineTimeout = 60 * 1000 * 10;
// if (typeof jasmine !== 'undefined') jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultJasmineTimeout;
const dbLogger = new Logger();
dbLogger.addTarget('console');
@@ -145,6 +147,8 @@ logger.addTarget('console');
logger.addTarget('file', { path: `${logDir}/log.txt` });
logger.setLevel(Logger.LEVEL_WARN); // Set to DEBUG to display sync process in console
Logger.initializeGlobalLogger(logger);
BaseItem.loadClass('Note', Note);
BaseItem.loadClass('Folder', Folder);
BaseItem.loadClass('Resource', Resource);
@@ -544,7 +548,7 @@ function asyncTest(callback) {
await callback();
} catch (error) {
if (error.constructor && error.constructor.name === 'ExpectationFailed') {
// OK - will be reported by Jasmine
// OK - will be reported by Jest
} else {
// Better to rethrow exception as stack trace is more useful in this case
throw error;
@@ -656,15 +660,17 @@ async function createTempDir() {
return tempDirPath;
}
function mockDate(year, month, day, tick) {
const fixedDate = new Date(2020, 0, 1);
jasmine.clock().install();
jasmine.clock().mockDate(fixedDate);
}
// TODO: Update for Jest
function restoreDate() {
jasmine.clock().uninstall();
}
// function mockDate(year, month, day, tick) {
// const fixedDate = new Date(2020, 0, 1);
// jasmine.clock().install();
// jasmine.clock().mockDate(fixedDate);
// }
// function restoreDate() {
// jasmine.clock().uninstall();
// }
// Application for feature integration testing
class TestApp extends BaseApplication {
@@ -734,4 +740,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, mockDate, restoreDate, TestApp };
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 };

View File

@@ -1,7 +1,6 @@
node_modules/
packageInfo.js
dist/
lib/
*.min.js
.DS_Store
gui/note-viewer/pluginAssets/

View File

@@ -87,6 +87,7 @@ export default class ElectronAppWrapper {
webPreferences: {
nodeIntegration: true,
spellcheck: true,
enableRemoteModule: true,
},
webviewTag: true,
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting

View File

@@ -490,17 +490,24 @@ class Application extends BaseApplication {
}
private async initPluginService() {
const pluginLogger = new Logger();
pluginLogger.addTarget(TargetType.File, { path: `${Setting.value('profileDir')}/log-plugins.txt` });
pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' });
pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO);
const service = PluginService.instance();
const pluginRunner = new PluginRunner();
PluginService.instance().setLogger(pluginLogger);
PluginService.instance().initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
// Users can add and remove plugins from the config screen at any
// time, however we only effectively uninstall the plugin the next
// time the app is started. What plugin should be uninstalled is
// stored in the settings.
const newSettings = await service.uninstallPlugins(pluginSettings);
Setting.setValue('plugins.states', newSettings);
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'));
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
}
@@ -508,12 +515,12 @@ class Application extends BaseApplication {
try {
if (Setting.value('plugins.devPluginPaths')) {
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
await PluginService.instance().loadAndRunPlugins(paths);
await service.loadAndRunPlugins(paths, pluginSettings, true);
}
// Also load dev plugins that have passed via command line arguments
if (Setting.value('startupDevPlugins')) {
await PluginService.instance().loadAndRunPlugins(Setting.value('startupDevPlugins'));
await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, true);
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
@@ -723,6 +730,14 @@ class Application extends BaseApplication {
// console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState()));
// }, 2000);
// this.dispatch({
// type: 'NAV_GO',
// routeName: 'Config',
// props: {
// defaultSection: 'plugins',
// },
// });
return null;
}

View File

@@ -87,7 +87,7 @@ export class Bridge {
return filePath;
}
showOpenDialog(options: any) {
showOpenDialog(options: any = null) {
const { dialog } = require('electron');
if (!options) options = {};
let fileType = 'file';
@@ -117,13 +117,16 @@ export class Bridge {
}
showConfirmMessageBox(message: string, options: any = null) {
if (options === null) options = {};
options = {
buttons: [_('OK'), _('Cancel')],
...options,
};
const result = this.showMessageBox_(this.window(), Object.assign({}, {
type: 'question',
message: message,
cancelId: 1,
buttons: [_('OK'), _('Cancel')],
buttons: options.buttons,
}, options));
return result === 0;
@@ -167,8 +170,8 @@ export class Bridge {
return require('electron').shell.openExternal(url);
}
openItem(fullPath: string) {
return require('electron').shell.openItem(fullPath);
async openItem(fullPath: string) {
return require('electron').shell.openPath(fullPath);
}
checkForUpdates(inBackground: boolean, window: any, logFilePath: string, options: any) {

View File

@@ -3,17 +3,23 @@ import SideBar from './SideBar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/PluginsStates';
const { connect } = require('react-redux');
const Setting = require('@joplin/lib/models/Setting').default;
const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils');
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry');
const shared = require('@joplin/lib/components/shared/config-shared.js');
const bridge = require('electron').remote.require('./bridge').default;
const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
const settingKeyToControl: any = {
'plugins.states': control_PluginsStates,
};
class ConfigScreenComponent extends React.Component<any, any> {
rowStyle_: any = null;
@@ -27,6 +33,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
selectedSectionName: 'general',
screenName: '',
changedSettingKeys: [],
needRestart: false,
};
this.rowStyle_ = {
@@ -41,6 +48,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
this.renderLabel = this.renderLabel.bind(this);
this.renderDescription = this.renderDescription.bind(this);
}
async checkSyncConfig_() {
@@ -261,6 +270,40 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
private labelStyle(themeId: number) {
const theme = themeStyle(themeId);
return Object.assign({}, theme.textStyle, {
display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
});
}
private descriptionStyle(themeId: number) {
const theme = themeStyle(themeId);
return Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
fontStyle: 'italic',
maxWidth: '70em',
marginTop: 5,
});
}
private renderLabel(themeId: number, label: string) {
const labelStyle = this.labelStyle(themeId);
return (
<div style={labelStyle}>
<label>{label}</label>
</div>
);
}
private renderDescription(themeId: number, description: string) {
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
}
settingToComponent(key: string, value: any) {
const theme = themeStyle(this.props.themeId);
@@ -270,13 +313,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
marginBottom: theme.mainPadding,
};
const labelStyle = Object.assign({}, theme.textStyle, {
display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
});
const labelStyle = this.labelStyle(this.props.themeId);
const subLabel = Object.assign({}, labelStyle, {
display: 'block',
@@ -297,13 +334,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
backgroundColor: theme.backgroundColor,
};
const descriptionStyle = Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
marginTop: 5,
fontStyle: 'italic',
maxWidth: '70em',
});
const textInputBaseStyle = Object.assign({}, controlStyle, {
fontFamily: theme.fontFamily,
border: '1px solid',
@@ -318,18 +348,39 @@ class ConfigScreenComponent extends React.Component<any, any> {
});
const updateSettingValue = (key: string, value: any) => {
// console.info(key + ' = ' + value);
return shared.updateSettingValue(this, key, value);
};
const md = Setting.settingMetadata(key);
if (md.needRestart) {
this.setState({ needRestart: true });
}
shared.updateSettingValue(this, key, value);
// Component key needs to be key+value otherwise it doesn't update when the settings change.
if (md.autoSave) {
shared.scheduleSaveSettings(this);
}
};
const md = Setting.settingMetadata(key);
const descriptionText = Setting.keyDescription(key, 'desktop');
const descriptionComp = descriptionText ? <div style={descriptionStyle}>{descriptionText}</div> : null;
const descriptionComp = this.renderDescription(this.props.themeId, descriptionText);
if (md.isEnum) {
if (settingKeyToControl[key]) {
const SettingComponent = settingKeyToControl[key];
return (
<div key={key} style={rowStyle}>
{this.renderLabel(this.props.themeId, md.label())}
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
<SettingComponent
metadata={md}
value={value}
themeId={this.props.themeId}
onChange={(event: any) => {
updateSettingValue(key, event.value);
}}
/>
</div>
);
} else if (md.isEnum) {
const items = [];
const settingOptions = md.options();
const array = this.keyValueToArray(settingOptions);
@@ -568,12 +619,33 @@ class ConfigScreenComponent extends React.Component<any, any> {
return output;
}
onApplyClick() {
shared.saveSettings(this);
private restartMessage() {
return _('The application must be restarted for these changes to take effect.');
}
onSaveClick() {
private async restartApp() {
await Setting.saveAll();
bridge().restart();
}
private async checkNeedRestart() {
if (this.state.needRestart) {
const doItNow = await bridge().showConfirmMessageBox(this.restartMessage(), {
buttons: [_('Do it now'), _('Later')],
});
if (doItNow) await this.restartApp();
}
}
async onApplyClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
}
async onSaveClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
this.props.dispatch({ type: 'NAV_BACK' });
}
@@ -621,6 +693,13 @@ class ConfigScreenComponent extends React.Component<any, any> {
const sections = shared.settingsSections({ device: 'desktop', settings });
const needRestartComp: any = this.state.needRestart ? (
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>
{this.restartMessage()}
<a style={{ ...theme.urlStyle, marginLeft: 10 }} href="#" onClick={() => { this.restartApp(); }}>{_('Restart now')}</a>
</div>
) : null;
return (
<div style={{ display: 'flex', flexDirection: 'row' }}>
<SideBar
@@ -630,6 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
/>
<div style={style}>
{screenComp}
{needRestartComp}
<div style={containerStyle}>{settingComps}</div>
<ButtonBar
hasChanges={hasChanges}

View File

@@ -0,0 +1,255 @@
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { _ } from '@joplin/lib/locale';
import styled from 'styled-components';
import ToggleButton from '../../lib/ToggleButton/ToggleButton';
import Button, { ButtonLevel } from '../../Button/Button';
import bridge from '../../../services/bridge';
import produce from 'immer';
const Root = styled.div`
display: flex;
flex-direction: column;
`;
const TableRoot = styled.div`
display: flex;
flex-wrap: wrap;
`;
const InstallButton = styled(Button)`
margin-bottom: 10px;
`;
const CellRoot = styled.div`
display: flex;
background-color: ${props => props.theme.backgroundColor};
flex-direction: column;
align-items: flex-start;
padding: 15px;
border: 1px solid ${props => props.theme.dividerColor};
border-radius: 6px;
width: 250px;
margin-right: 20px;
margin-bottom: 20px;
box-shadow: 1px 1px 3px rgba(0,0,0,0.2);
`;
const CellTop = styled.div`
display: flex;
flex-direction: row;
width: 100%;
margin-bottom: 10px;
`;
const CellContent = styled.div`
display: flex;
margin-bottom: 10px;
flex: 1;
`;
const CellFooter = styled.div`
display: flex;
flex-direction: row;
`;
const DevModeLabel = styled.div`
border: 1px solid ${props => props.theme.color};
border-radius: 4px;
padding: 4px 6px;
font-size: ${props => props.theme.fontSize * 0.75}px;
color: ${props => props.theme.color};
`;
const StyledName = styled.div`
font-family: ${props => props.theme.fontFamily};
color: ${props => props.theme.color};
font-size: ${props => props.theme.fontSize}px;
font-weight: bold;
flex: 1;
`;
const StyledDescription = styled.div`
font-family: ${props => props.theme.fontFamily};
color: ${props => props.theme.colorFaded};
font-size: ${props => props.theme.fontSize}px;
line-height: 1.6em;
`;
interface Props {
value: any;
themeId: number;
onChange: Function;
}
interface CellProps {
item: PluginItem;
themeId: number;
onToggle: Function;
onDelete: Function;
}
interface PluginItem {
id: string;
name: string;
description: string;
enabled: boolean;
deleted: boolean;
devMode: boolean;
}
function Cell(props: CellProps) {
const { item } = props;
// For plugins in dev mode things like enabling/disabling or
// uninstalling them doesn't make sense, as that should be done by
// adding/removing them from wherever they were loaded from.
function renderToggleButton() {
if (item.devMode) {
return <DevModeLabel>DEV</DevModeLabel>;
}
return <ToggleButton
themeId={props.themeId}
value={item.enabled}
onToggle={() => props.onToggle({ item: props.item })}
/>;
}
function renderFooter() {
if (item.devMode) return null;
return (
<CellFooter>
<Button level={ButtonLevel.Secondary} onClick={() => props.onDelete({ item: props.item })} title={_('Delete')}/>
<div style={{ display: 'flex', flex: 1 }}/>
</CellFooter>
);
}
return (
<CellRoot>
<CellTop>
<StyledName mb={'5px'}>{item.name} {item.deleted ? '(Deleted)' : ''}</StyledName>
{renderToggleButton()}
</CellTop>
<CellContent>
<StyledDescription>{item.description}</StyledDescription>
</CellContent>
{renderFooter()}
</CellRoot>
);
}
function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[] {
return useMemo(() => {
const output: PluginItem[] = [];
for (const pluginId in plugins) {
const plugin = plugins[pluginId];
const setting: PluginSetting = {
...defaultPluginSetting(),
...settings[pluginId],
};
output.push({
id: pluginId,
name: plugin.manifest.name,
description: plugin.manifest.description,
enabled: setting.enabled,
deleted: setting.deleted,
devMode: plugin.devMode,
});
}
output.sort((a: PluginItem, b: PluginItem) => {
return a.name < b.name ? -1 : +1;
});
return output;
}, [plugins, settings]);
}
export default function(props: Props) {
const pluginService = PluginService.instance();
const pluginSettings = useMemo(() => {
return pluginService.unserializePluginSettings(props.value);
}, [props.value]);
const onDelete = useCallback(async (event: any) => {
const item: PluginItem = event.item;
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.name));
if (!confirm) return;
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
draft[item.id].deleted = true;
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
const onToggle = useCallback((event: any) => {
const item: PluginItem = event.item;
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
draft[item.id].enabled = !draft[item.id].enabled;
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
const onInstall = useCallback(async () => {
const result = bridge().showOpenDialog({
filters: [{ name: 'Joplin Plugin Archive', extensions: ['jpl'] }],
});
const filePath = result && result.length ? result[0] : null;
if (!filePath) return;
const plugin = await pluginService.installPlugin(filePath);
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
draft[plugin.manifest.id] = defaultPluginSetting();
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
function renderCells(items: PluginItem[]) {
const output = [];
for (const item of items) {
if (item.deleted) continue;
output.push(<Cell
key={item.id}
item={item}
themeId={props.themeId}
onDelete={onDelete}
onToggle={onToggle}
/>);
}
return output;
}
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
return (
<Root>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<InstallButton level={ButtonLevel.Primary} onClick={onInstall} title={_('Install plugin')}/>
<div style={{ display: 'flex', flex: 1 }}/>
</div>
<TableRoot>
{renderCells(pluginItems)}
</TableRoot>
</Root>
);
}

View File

@@ -6,7 +6,7 @@ const { themeStyle } = require('@joplin/lib/theme');
const { _ } = require('@joplin/lib/locale');
const time = require('@joplin/lib/time').default;
const shim = require('@joplin/lib/shim').default;
const dialogs = require('./dialogs');
const dialogs = require('./dialogs').default;
const shared = require('@joplin/lib/components/shared/encryption-config-shared.js');
const bridge = require('electron').remote.require('./bridge').default;

View File

@@ -16,12 +16,12 @@ import { MenuItem, MenuItemLocation } from '@joplin/lib/services/plugins/api/typ
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import menuCommandNames from './menuCommandNames';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
import bridge from '../services/bridge';
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
const packageInfo = require('../packageInfo.js');
const bridge = require('electron').remote.require('./bridge').default;
const { shell, clipboard } = require('electron');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
const PluginManager = require('@joplin/lib/services/PluginManager');
const TemplateUtils = require('@joplin/lib/TemplateUtils');
@@ -300,7 +300,7 @@ function useMenu(props: Props) {
}, {
label: _('Open template directory'),
click: () => {
shell.openItem(Setting.value('templateDir'));
bridge().openItem(Setting.value('templateDir'));
},
}, {
label: _('Refresh templates'),

View File

@@ -22,6 +22,8 @@ import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import { themeStyle } from '@joplin/lib/theme';
import { ThemeAppearance } from '@joplin/lib/themes/type';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import dialogs from '../../../dialogs';
const Note = require('@joplin/lib/models/Note.js');
const { clipboard } = require('electron');
@@ -29,7 +31,6 @@ const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { reg } = require('@joplin/lib/registry.js');
const dialogs = require('../../../dialogs');
const menuUtils = new MenuUtils(CommandService.instance());
@@ -255,54 +256,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
}
}, []);
const onEditorContextMenu = useCallback(() => {
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
const clipboardText = clipboard.readText();
menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
editorCutText();
},
})
);
menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
editorCopyText();
},
})
);
menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
if (clipboardText) {
editorPasteText();
} else {
// To handle pasting images
onEditorPaste();
}
},
})
);
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
menu.append(new MenuItem(item));
});
menu.popup(bridge().window());
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, props.plugins]);
const loadScript = async (script: any) => {
return new Promise((resolve) => {
let element: any = document.createElement('script');
@@ -409,6 +362,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
padding-bottom: 400px !important;
}
.CodeMirror-sizer {
/* Add a fixed right padding to account for the appearance (and disappearance) */
/* of the sidebar */
padding-right: 10px !important;
}
.cm-header-1 {
font-size: 1.5em;
}
@@ -623,6 +582,91 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
editorRef.current.refresh();
}, [rootSize, styles.editor, props.visiblePanes]);
// The below code adds support for spellchecking when it is enabled
// It might be buggy, refer to the below issue
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
useEffect(() => {
function pointerInsideEditor(x: number, y: number) {
const elements = document.getElementsByClassName('codeMirrorEditor');
if (!elements.length) return null;
const rect = elements[0].getBoundingClientRect();
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
}
function onContextMenu(_event: any, params: any) {
if (!pointerInsideEditor(params.x, params.y)) return;
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
const clipboardText = clipboard.readText();
menu.append(
new MenuItem({
label: _('Cut'),
enabled: hasSelectedText,
click: async () => {
editorCutText();
},
})
);
menu.append(
new MenuItem({
label: _('Copy'),
enabled: hasSelectedText,
click: async () => {
editorCopyText();
},
})
);
menu.append(
new MenuItem({
label: _('Paste'),
enabled: true,
click: async () => {
if (clipboardText) {
editorPasteText();
} else {
// To handle pasting images
onEditorPaste();
}
},
})
);
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {
menu.append(new MenuItem(item));
}
// Typically CodeMirror handles all interactions itself (highlighting etc.)
// But in the case of clicking a mispelled word, we need electron to handle the click
// The result is that CodeMirror doesn't know what's been selected and doesn't
// move the cursor into the correct location.
// and when the user selects a new spelling it will be inserted in the wrong location
// So in this situation, we use must manually align the internal codemirror selection
// to the contextmenu selection
if (editorRef.current && spellCheckerMenuItems.length > 0) {
editorRef.current.alignSelection(params);
}
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
menu.append(new MenuItem(item));
});
menu.popup();
}
bridge().window().webContents.on('context-menu', onContextMenu);
return () => {
bridge().window().webContents.off('context-menu', onContextMenu);
};
}, []);
function renderEditor() {
return (
@@ -640,7 +684,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
plugins={props.plugins}
onChange={codeMirror_change}
onScroll={editor_scroll}
onEditorContextMenu={onEditorContextMenu}
onEditorPaste={onEditorPaste}
/>
</div>

View File

@@ -27,6 +27,8 @@ import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown
import 'codemirror/mode/meta';
import Setting from '@joplin/lib/models/Setting';
// import eventManager from '@joplin/lib/eventManager';
const { reg } = require('@joplin/lib/registry.js');
@@ -77,19 +79,18 @@ for (let i = 0; i < topLanguages.length; i++) {
}
export interface EditorProps {
value: string,
searchMarkers: any,
mode: string,
style: any,
codeMirrorTheme: any,
readOnly: boolean,
autoMatchBraces: boolean,
keyMap: string,
plugins: PluginStates,
onChange: any,
onScroll: any,
onEditorContextMenu: any,
onEditorPaste: any,
value: string;
searchMarkers: any;
mode: string;
style: any;
codeMirrorTheme: any;
readOnly: boolean;
autoMatchBraces: boolean;
keyMap: string;
plugins: PluginStates;
onChange: any;
onScroll: any;
onEditorPaste: any;
}
function Editor(props: EditorProps, ref: any) {
@@ -122,13 +123,6 @@ function Editor(props: EditorProps, ref: any) {
props.onScroll();
}, [props.onScroll]);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const editor_mousedown = useCallback((_cm: any, event: any) => {
if (event && event.button === 2) {
props.onEditorContextMenu();
}
}, [props.onEditorContextMenu]);
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
const editor_paste = useCallback((_cm: any, _event: any) => {
props.onEditorPaste();
@@ -163,7 +157,7 @@ function Editor(props: EditorProps, ref: any) {
mode: props.mode,
readOnly: props.readOnly,
autoCloseBrackets: props.autoMatchBraces,
inputStyle: 'textarea', // contenteditable loses cursor position on focus change, use textarea instead
inputStyle: Setting.value('editor.spellcheckBeta') ? 'contenteditable' : 'textarea',
lineWrapping: true,
lineNumbers: false,
indentWithTabs: true,
@@ -177,7 +171,6 @@ function Editor(props: EditorProps, ref: any) {
setEditor(cm);
cm.on('change', editor_change);
cm.on('scroll', editor_scroll);
cm.on('mousedown', editor_mousedown);
cm.on('paste', editor_paste);
cm.on('drop', editor_drop);
cm.on('dragover', editor_drag);
@@ -191,7 +184,6 @@ function Editor(props: EditorProps, ref: any) {
// Clean up codemirror
cm.off('change', editor_change);
cm.off('scroll', editor_scroll);
cm.off('mousedown', editor_mousedown);
cm.off('paste', editor_paste);
cm.off('drop', editor_drop);
cm.off('dragover', editor_drag);
@@ -250,7 +242,7 @@ function Editor(props: EditorProps, ref: any) {
}
}, [pluginOptions, editor]);
return <div style={props.style} ref={editorParent} />;
return <div className='codeMirrorEditor' style={props.style} ref={editorParent} />;
}
export default forwardRef(Editor);

View File

@@ -100,5 +100,33 @@ export default function useCursorUtils(CodeMirror: any) {
});
});
// params are the oncontextmenu params
CodeMirror.defineExtension('alignSelection', function(params: any) {
// The below is a HACK that uses the selectionText from electron and the coordinates of
// the click to determine what the codemirror selection should be
const alignStrings = (s1: string, s2: string) => {
for (let i = 0; i < s1.length; i++) {
if (s1.substr(i, s2.length) === s2) { return i; }
}
return -1;
};
const selectionText = params.selectionText;
const coords = this.coordsChar({ left: params.x, top: params.y });
const { anchor, head } = this.findWordAt(coords);
const selectedWord = this.getRange(anchor, head);
if (selectionText.length > selectedWord.length) {
const offset = alignStrings(selectionText, selectedWord);
anchor.ch -= offset;
head.ch = anchor.ch + selectionText.length;
} else if (selectionText.length < selectedWord.length) {
const offset = alignStrings(selectedWord, selectionText);
anchor.ch += offset;
head.ch = anchor.ch + selectionText.length;
}
this.setSelection(anchor, head);
});
}

View File

@@ -4,6 +4,7 @@ const { buildStyle } = require('@joplin/lib/theme');
export default function styles(props: NoteBodyEditorProps) {
return buildStyle(['TinyMCE', props.style.width, props.style.height], props.themeId, (theme: any) => {
const extraToolbarContainer = {
boxSizing: 'content-box',
backgroundColor: theme.backgroundColor3,
display: 'flex',
flexDirection: 'row',

View File

@@ -15,7 +15,6 @@ const { render } = require('react-dom');
const { connect, Provider } = require('react-redux');
const Setting = require('@joplin/lib/models/Setting').default;
const shim = require('@joplin/lib/shim').default;
shim.setReact(React);
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
const { Navigator } = require('./Navigator.min.js');
@@ -32,6 +31,10 @@ interface Props {
}
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
div, span, a {
/*color: ${(props: any) => props.theme.color};*/
/*font-size: ${(props: any) => props.theme.fontSize}px;*/

View File

@@ -1,20 +1,20 @@
const smalltalk = require('smalltalk');
class Dialogs {
async alert(message, title = '') {
async alert(message: string, title = '') {
await smalltalk.alert(title, message);
}
async confirm(message, title = '') {
async confirm(message: string, title = '', options: any = {}) {
try {
await smalltalk.confirm(title, message);
await smalltalk.confirm(title, message, options);
return true;
} catch (error) {
return false;
}
}
async prompt(message, title = '', defaultValue = '', options = null) {
async prompt(message: string, title = '', defaultValue = '', options: any = null) {
if (options === null) options = {};
try {
@@ -28,4 +28,4 @@ class Dialogs {
const dialogs = new Dialogs();
module.exports = dialogs;
export default dialogs;

View File

@@ -0,0 +1,37 @@
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
const ReactToggleButton = require('react-toggle-button');
const Color = require('color');
interface Props {
value: boolean;
onToggle: Function;
themeId: number;
}
export default function(props: Props) {
const theme = themeStyle(props.themeId);
return (
<ReactToggleButton
value={props.value}
onToggle={props.onToggle}
colors={{
activeThumb: {
base: Color(theme.color5).rgb().string(),
},
active: {
base: Color(theme.backgroundColor5).alpha(0.7).rgb().string(),
},
}}
trackStyle={{
opacity: props.value ? 1 : 0.3,
}}
thumbStyle={{
opacity: props.value ? 1 : 0.5,
}}
inactiveLabel=""
activeLabel=""
/>
);
}

View File

@@ -27,6 +27,7 @@ const { shimInit } = require('@joplin/lib/shim-init-node.js');
const EncryptionService = require('@joplin/lib/services/EncryptionService');
const bridge = require('electron').remote.require('./bridge').default;
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
const React = require('react');
if (bridge().env() === 'dev') {
const newConsole = function(oldConsole) {
@@ -86,7 +87,7 @@ try {
keytar = null;
}
shimInit(null, keytar);
shimInit(null, keytar, React);
// Disable drag and drop of links inside application (which would
// open it as if the whole app was a browser)

File diff suppressed because it is too large Load Diff

View File

@@ -89,7 +89,6 @@
"devDependencies": {
"@joplin/tools": "^1.0.9",
"@testing-library/react-hooks": "^3.4.2",
"@types/jasmine": "^3.5.11",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
"@types/react": "16.9.55",
@@ -98,9 +97,9 @@
"app-builder-bin": "^1.9.11",
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
"electron": "^8.2.5",
"electron-builder": "22.3.2",
"electron-rebuild": "^1.10.1",
"electron": "^10.1.6",
"electron-builder": "22.9.1",
"electron-rebuild": "^2.3.2",
"glob": "^7.1.6",
"gulp": "^4.0.2",
"jest": "^26.6.3",
@@ -144,6 +143,7 @@
"react-dom": "16.9.0",
"react-redux": "^5.0.7",
"react-select": "^2.4.3",
"react-toggle-button": "^2.2.0",
"react-tooltip": "^3.10.0",
"redux": "^3.7.2",
"reselect": "^4.0.0",

View File

@@ -100,9 +100,11 @@ export default class PluginRunner extends BasePluginRunner {
slashes: true,
})}?pluginId=${encodeURIComponent(plugin.id)}&pluginScript=${encodeURIComponent(`file://${scriptPath}`)}`);
pluginWindow.webContents.once('dom-ready', () => {
pluginWindow.webContents.openDevTools({ mode: 'detach' });
});
if (plugin.devMode) {
pluginWindow.webContents.once('dom-ready', () => {
pluginWindow.webContents.openDevTools({ mode: 'detach' });
});
}
ipcRenderer.on('pluginMessage', async (_event: any, message: PluginMessage) => {
if (message.target !== PluginMessageTarget.MainWindow) return;

View File

@@ -399,6 +399,8 @@ async function initialize(dispatch) {
mainLogger.setLevel(Logger.LEVEL_DEBUG);
}
Logger.initializeGlobalLogger(mainLogger);
reg.setLogger(mainLogger);
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });

View File

@@ -4,6 +4,29 @@ const Generator = require('yeoman-generator');
const chalk = require('chalk');
const yosay = require('yosay');
function mergePackageKey(parentKey, source, dest) {
const output = Object.assign({}, dest);
for (const k in source) {
if (!(k in output)) {
// If the key doesn't exist in the destination, add it
output[k] = source[k];
} else if (parentKey === 'devDependencies') {
// If we are dealing with the dependencies, overwrite with the
// version from source.
output[k] = source[k];
} else if (typeof source[k] === 'object' && !Array.isArray(k) && source[k] !== null) {
// If it's an object, recursively process it
output[k] = mergePackageKey(k, source[k], output[k]);
} else {
// Otherwise, the default is to preserve the destination key
output[k] = dest[k];
}
}
return output;
}
module.exports = class extends Generator {
constructor(args, opts) {
@@ -18,8 +41,6 @@ module.exports = class extends Generator {
}
}
async prompting() {
this.log(yosay(`Welcome to the fine ${chalk.red('generator-joplin')} generator!`));
@@ -110,12 +131,28 @@ module.exports = class extends Generator {
if (this.options.update && noUpdateFiles.includes(file)) continue;
const destFile = file.replace(/_TEMPLATE/, '');
const destFilePath = this.destinationPath(destFile);
this.fs.copyTpl(
this.templatePath(file),
this.destinationPath(destFile),
this.props
);
if (this.options.update && destFile === 'package.json' && this.fs.exists(destFilePath)) {
const destContent = this.fs.readJSON(destFilePath);
this.fs.copy(
this.templatePath(file),
destFilePath, {
process: (sourceBuffer) => {
const sourceContent = JSON.parse(sourceBuffer.toString());
const newContent = mergePackageKey(null, sourceContent, destContent);
return JSON.stringify(newContent, null, 2);
},
}
);
} else {
this.fs.copyTpl(
this.templatePath(file),
destFilePath,
this.props
);
}
}
this.fs.copy(

View File

@@ -46,13 +46,13 @@ const MigrationService = require('./services/MigrationService');
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
const appLogger = Logger.create('App');
// const ntpClient = require('./vendor/ntp-client');
// ntpClient.dgram = require('dgram');
export default class BaseApplication {
private logger_: Logger;
private dbLogger_: Logger;
private eventEmitter_: any;
private scheduleAutoAddResourcesIID_: any = null;
private database_: any = null;
@@ -68,10 +68,7 @@ export default class BaseApplication {
protected store_: any = null;
constructor() {
this.logger_ = new Logger();
this.dbLogger_ = new Logger();
this.eventEmitter_ = new EventEmitter();
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = this.decryptionWorker_resourceMetadataButNotBlobDecrypted.bind(this);
}
@@ -101,15 +98,13 @@ export default class BaseApplication {
EncryptionService.instance_ = null;
DecryptionWorker.instance_ = null;
this.logger_.info('Base application terminated...');
this.logger_ = null;
this.dbLogger_ = null;
appLogger.info('Base application terminated...');
this.eventEmitter_ = null;
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = null;
}
logger() {
return this.logger_;
return appLogger;
}
public store() {
@@ -289,7 +284,7 @@ export default class BaseApplication {
parentType = BaseModel.TYPE_SMART_FILTER;
}
this.logger().debug('Refreshing notes:', parentType, parentId);
appLogger.debug('Refreshing notes:', parentType, parentId);
const options = {
order: stateUtils.notesOrder(state.settings),
@@ -460,7 +455,7 @@ export default class BaseApplication {
}
async generalMiddleware(store: any, next: any, action: any) {
// this.logger().debug('Reducer action', this.reducerActionToString(action));
// appLogger.debug('Reducer action', this.reducerActionToString(action));
const result = next(action);
const newState = store.getState();
@@ -547,7 +542,11 @@ export default class BaseApplication {
}
if (action.type === 'NOTE_UPDATE_ONE') {
if (!action.changedFields.length || action.changedFields.includes('parent_id') || action.changedFields.includes('encryption_applied')) {
if (!action.changedFields.length ||
action.changedFields.includes('parent_id') ||
action.changedFields.includes('encryption_applied') ||
action.changedFields.includes('is_conflict')
) {
refreshFolders = true;
}
}
@@ -701,35 +700,30 @@ export default class BaseApplication {
const extraFlags = await this.readFlagsFromFile(`${profileDir}/flags.txt`);
initArgs = Object.assign(initArgs, extraFlags);
this.logger_.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
if (Setting.value('env') === 'dev' && !shim.isTestingEnv()) {
// this.logger_.addTarget(TargetType.Console, { level: Logger.LEVEL_DEBUG });
}
this.logger_.setLevel(initArgs.logLevel);
reg.setLogger(this.logger_);
const globalLogger = new Logger();
globalLogger.addTarget(TargetType.File, { path: `${profileDir}/log.txt` });
if (Setting.value('appType') === 'desktop') {
globalLogger.addTarget(TargetType.Console);
}
globalLogger.setLevel(initArgs.logLevel);
Logger.initializeGlobalLogger(globalLogger);
reg.setLogger(Logger.create(''));
reg.dispatch = () => {};
BaseService.logger_ = this.logger_;
// require('lib/ntpDate').setLogger(reg.logger());
BaseService.logger_ = globalLogger;
this.dbLogger_.addTarget(TargetType.File, { path: `${profileDir}/log-database.txt` });
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('appType') === 'desktop') {
this.logger_.addTarget(TargetType.Console, { level: Logger.LEVEL_WARN });
this.dbLogger_.addTarget(TargetType.Console, { level: Logger.LEVEL_WARN });
}
if (Setting.value('env') === 'dev') {
this.dbLogger_.setLevel(Logger.LEVEL_INFO);
}
this.logger_.info(`Profile directory: ${profileDir}`);
appLogger.info(`Profile directory: ${profileDir}`);
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
this.database_.setLogExcludedQueryTypes(['SELECT']);
this.database_.setLogger(this.dbLogger_);
this.database_.setLogger(globalLogger);
// if (Setting.value('env') === 'dev') {
// if (shim.isElectron()) {
@@ -756,7 +750,7 @@ export default class BaseApplication {
await loadKeychainServiceAndSettings(KeychainServiceDriver);
this.logger_.info(`Client ID: ${Setting.value('clientId')}`);
appLogger.info(`Client ID: ${Setting.value('clientId')}`);
if (Setting.value('firstStart')) {
const locale = shim.detectAndSetLocale(Setting);
@@ -817,9 +811,9 @@ export default class BaseApplication {
KvStore.instance().setDb(reg.db());
EncryptionService.instance().setLogger(this.logger_);
EncryptionService.instance().setLogger(globalLogger);
BaseItem.encryptionService_ = EncryptionService.instance();
DecryptionWorker.instance().setLogger(this.logger_);
DecryptionWorker.instance().setLogger(globalLogger);
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
DecryptionWorker.instance().setKvStore(KvStore.instance());
await EncryptionService.instance().loadMasterKeysFromSettings();
@@ -828,7 +822,7 @@ export default class BaseApplication {
ResourceFetcher.instance().setFileApi(() => {
return reg.syncTarget().fileApi();
});
ResourceFetcher.instance().setLogger(this.logger_);
ResourceFetcher.instance().setLogger(globalLogger);
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
ResourceFetcher.instance().start();

View File

@@ -26,6 +26,13 @@ interface Target {
source?: string;
}
interface LoggerWrapper {
debug: Function;
info: Function;
warn: Function;
error: Function;
}
class Logger {
// For backward compatibility
@@ -36,6 +43,7 @@ class Logger {
public static LEVEL_DEBUG = LogLevel.Debug;
public static fsDriver_: any = null;
private static globalLogger_: Logger = null;
private targets_: Target[] = [];
private level_: LogLevel = LogLevel.Info;
@@ -46,6 +54,24 @@ class Logger {
return Logger.fsDriver_;
}
public static initializeGlobalLogger(logger: Logger) {
this.globalLogger_ = logger;
}
private static get globalLogger(): Logger {
if (!this.globalLogger_) throw new Error('Global logger has not been initialized!!');
return this.globalLogger_;
}
static create(prefix: string): LoggerWrapper {
return {
debug: (...object: any[]) => this.globalLogger.log(LogLevel.Debug, prefix, ...object),
info: (...object: any[]) => this.globalLogger.log(LogLevel.Info, prefix, ...object),
warn: (...object: any[]) => this.globalLogger.log(LogLevel.Warn, prefix, ...object),
error: (...object: any[]) => this.globalLogger.log(LogLevel.Error, prefix, ...object),
};
}
setLevel(level: LogLevel) {
this.level_ = level;
}
@@ -132,14 +158,12 @@ class Logger {
return this.level();
}
log(level: LogLevel, ...object: any[]) {
public log(level: LogLevel, prefix: string, ...object: any[]) {
if (!this.targets_.length) return;
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
const line = `${timestamp}: `;
for (let i = 0; i < this.targets_.length; i++) {
const target = this.targets_[i];
const targetPrefix = prefix ? prefix : target.prefix;
if (this.targetLevel(target) < level) continue;
@@ -149,26 +173,30 @@ class Logger {
if (level == LogLevel.Warn) fn = 'warn';
if (level == LogLevel.Info) fn = 'info';
const consoleObj = target.console ? target.console : console;
let items = [moment().format('HH:mm:ss')];
if (target.prefix) {
items.push(target.prefix);
}
items = items.concat(...object);
const prefixItems = [moment().format('HH:mm:ss')];
if (targetPrefix) prefixItems.push(targetPrefix);
const items = [`${prefixItems.join(': ')}:`].concat(...object);
consoleObj[fn](...items);
} else if (target.type == 'file') {
const serializedObject = this.objectsToString(...object);
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
const line = [timestamp];
if (targetPrefix) line.push(targetPrefix);
line.push(this.objectsToString(...object));
try {
Logger.fsDriver().appendFileSync(target.path, `${line + serializedObject}\n`);
// TODO: Should log async
Logger.fsDriver().appendFileSync(target.path, `${line.join(': ')}\n`);
} catch (error) {
console.error('Cannot write to log file:', error);
}
} else if (target.type == 'database') {
const msg = this.objectsToString(...object);
const msg = [];
if (targetPrefix) msg.push(targetPrefix);
msg.push(this.objectsToString(...object));
const queries = [
{
sql: 'INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)',
params: [target.source, level, msg, time.unixMs()],
params: [target.source, level, msg.join(': '), time.unixMs()],
},
];
@@ -188,16 +216,16 @@ class Logger {
}
error(...object: any[]) {
return this.log(LogLevel.Error, ...object);
return this.log(LogLevel.Error, null, ...object);
}
warn(...object: any[]) {
return this.log(LogLevel.Warn, ...object);
return this.log(LogLevel.Warn, null, ...object);
}
info(...object: any[]) {
return this.log(LogLevel.Info, ...object);
return this.log(LogLevel.Info, null, ...object);
}
debug(...object: any[]) {
return this.log(LogLevel.Debug, ...object);
return this.log(LogLevel.Debug, null, ...object);
}
static levelStringToId(s: string) {

View File

@@ -92,6 +92,15 @@ shared.updateSettingValue = function(comp, key, value) {
});
};
shared.scheduleSaveSettings = function(comp) {
if (shared.scheduleSaveSettingsIID) clearTimeout(shared.scheduleSaveSettingsIID);
shared.scheduleSaveSettingsIID = setTimeout(() => {
shared.scheduleSaveSettingsIID = null;
shared.saveSettings(comp);
}, 100);
};
shared.saveSettings = function(comp) {
for (const key in comp.state.settings) {
if (!comp.state.settings.hasOwnProperty(key)) continue;

View File

@@ -6,6 +6,7 @@ const shim = require('./shim').default;
class Database {
constructor(driver) {
this.debugMode_ = false;
this.sqlQueryLogEnabled_ = false;
this.driver_ = driver;
this.logger_ = new Logger();
this.logExcludedQueryTypes_ = [];
@@ -238,6 +239,8 @@ class Database {
}
logQuery(sql, params = null) {
if (!this.sqlQueryLogEnabled_) return;
if (this.logExcludedQueryTypes_.length) {
const temp = sql.toLowerCase();
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {

View File

@@ -276,7 +276,7 @@ class Note extends BaseItem {
includeTimestamps: true,
}, options);
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language'];
const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language', 'is_conflict'];
if (options.includeTimestamps) {
output.push('updated_time');

View File

@@ -33,7 +33,7 @@ export interface SettingItem {
isEnum?: boolean;
section?: string;
label?(): string;
description?(appType: string): string;
description?: Function;
options?(): any;
appTypes?: string[];
show?(settings: any): boolean;
@@ -44,7 +44,9 @@ export interface SettingItem {
maximum?: number;
step?: number;
onClick?(): void;
unitLabel?(value: any): string;
unitLabel?: Function;
needRestart?: boolean;
autoSave?: boolean;
}
interface SettingItems {
@@ -554,6 +556,17 @@ class Setting extends BaseModel {
},
},
'plugins.states': {
value: '',
type: SettingItemType.Object,
section: 'plugins',
public: true,
appTypes: ['desktop'],
label: () => _('Plugins'),
needRestart: true,
autoSave: true,
},
'plugins.devPluginPaths': {
value: '',
type: SettingItemType.String,
@@ -702,7 +715,7 @@ class Setting extends BaseModel {
description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.',
},
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, section: 'application', public: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') },
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, public: false },
'sync.interval': {
@@ -762,6 +775,16 @@ class Setting extends BaseModel {
},
},
'editor.spellcheckBeta': {
value: false,
type: SettingItemType.Bool,
public: true,
appTypes: ['desktop'],
advanced: true,
label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)',
description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)',
},
'net.customCertificates': {
value: '',
type: SettingItemType.String,
@@ -816,7 +839,7 @@ class Setting extends BaseModel {
minimum: 1,
maximum: 365 * 2,
step: 1,
unitLabel: (value = null) => {
unitLabel: (value: number = null) => {
return value === null ? _('days') : _('%d days', value);
},
label: () => _('Keep note history for'),

View File

@@ -3,7 +3,7 @@ import { Pagination, PaginationOrder } from './types';
export interface ModelFeedPage {
items: any[];
has_more?: boolean;
has_more: boolean;
total?: number;
}
@@ -52,9 +52,8 @@ export default async function(db: any, tableName: string, pagination: Pagination
const rows = await db.selectAll(sql, sqlParams);
const output: ModelFeedPage = { items: rows };
if (rows.length >= pagination.limit) output.has_more = true;
return output;
return {
items: rows,
has_more: rows.length >= pagination.limit,
};
}

View File

@@ -98,35 +98,6 @@
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"argparse": {
@@ -270,6 +241,15 @@
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
"optional": true,
"requires": {
"inherits": "~2.0.0"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -531,6 +511,14 @@
"whatwg-url": "^7.0.0"
}
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -753,6 +741,18 @@
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"optional": true
},
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"optional": true,
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
}
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -1512,6 +1512,11 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multiparty": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
@@ -1527,11 +1532,6 @@
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.0.1.tgz",
"integrity": "sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA=="
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
},
"nanoid": {
"version": "3.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.16.tgz",
@@ -1545,21 +1545,6 @@
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"no-case": {
@@ -1570,6 +1555,11 @@
"lower-case": "^1.1.1"
}
},
"node-addon-api": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA=="
},
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@@ -1587,6 +1577,39 @@
"is-stream": "^1.0.1"
}
},
"node-gyp": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
"integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
"optional": true,
"requires": {
"fstream": "^1.0.0",
"glob": "^7.0.3",
"graceful-fs": "^4.1.2",
"mkdirp": "^0.5.0",
"nopt": "2 || 3",
"npmlog": "0 || 1 || 2 || 3 || 4",
"osenv": "0",
"request": "^2.87.0",
"rimraf": "2",
"semver": "~5.3.0",
"tar": "^2.0.0",
"which": "1"
},
"dependencies": {
"tar": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"optional": true,
"requires": {
"block-stream": "*",
"fstream": "^1.0.12",
"inherits": "2"
}
}
}
},
"node-notifier": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.0.tgz",
@@ -1645,15 +1668,26 @@
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
},
"dependencies": {
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
}
}
},
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
"integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"optional": true,
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
"abbrev": "1"
}
},
"normalize-path": {
@@ -1947,6 +1981,27 @@
"safe-buffer": "^5.1.1"
}
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
@@ -2098,9 +2153,9 @@
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
},
"server-destroy": {
"version": "1.0.1",
@@ -2168,11 +2223,12 @@
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
},
"sqlite3": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz",
"integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.0.tgz",
"integrity": "sha512-rjvqHFUaSGnzxDy2AHCwhHy6Zp6MNJzCPGYju4kD8yi6bze4d1/zMTg6C7JI49b7/EM7jKMTvyfN/4ylBKdwfw==",
"requires": {
"nan": "^2.12.1",
"node-addon-api": "2.0.0",
"node-gyp": "3.x",
"node-pre-gyp": "^0.11.0"
}
},
@@ -2275,6 +2331,21 @@
}
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"stringify-parameters": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stringify-parameters/-/stringify-parameters-0.0.4.tgz",
@@ -2644,6 +2715,15 @@
"webidl-conversions": "^4.0.2"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"optional": true,
"requires": {
"isexe": "^2.0.0"
}
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",

View File

@@ -64,7 +64,7 @@
"reselect": "^4.0.0",
"server-destroy": "^1.0.1",
"sprintf-js": "^1.1.2",
"sqlite3": "^4.1.1",
"sqlite3": "^5.0.0",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"tar": "^4.4.10",

View File

@@ -787,8 +787,14 @@ const reducer = produce((draft: Draft<State> = defaultState, action: any) => {
for (let i = 0; i < newNotes.length; i++) {
const n = newNotes[i];
if (n.id == modNote.id) {
// Note is still in the same folder
if (isViewingAllNotes || noteIsInFolder(modNote, n.parent_id)) {
if (n.is_conflict && !modNote.is_conflict) {
// Note was a conflict but was moved outside of
// the conflict folder
newNotes.splice(i, 1);
noteFolderHasChanged = true;
movedNotePreviousIndex = i;
} else if (isViewingAllNotes || noteIsInFolder(modNote, n.parent_id)) {
// Note is still in the same folder
// Merge the properties that have changed (in modNote) into
// the object we already have.
newNotes[i] = Object.assign({}, newNotes[i]);

View File

@@ -6,6 +6,8 @@ import { ContentScriptType } from './api/types';
import Logger from '../../Logger';
const EventEmitter = require('events');
const logger = Logger.create('Plugin');
interface ViewControllers {
[key: string]: ViewController;
}
@@ -21,37 +23,33 @@ interface ContentScripts {
export default class Plugin {
private id_: string;
private baseDir_: string;
private manifest_: PluginManifest;
private scriptText_: string;
private enabled_: boolean = true;
private logger_: Logger = null;
private viewControllers_: ViewControllers = {};
private contentScripts_: ContentScripts = {};
private dispatch_: Function;
private eventEmitter_: any;
private devMode_: boolean = false;
constructor(id: string, baseDir: string, manifest: PluginManifest, scriptText: string, logger: Logger, dispatch: Function) {
this.id_ = id;
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, dispatch: Function) {
this.baseDir_ = shim.fsDriver().resolve(baseDir);
this.manifest_ = manifest;
this.scriptText_ = scriptText;
this.logger_ = logger;
this.dispatch_ = dispatch;
this.eventEmitter_ = new EventEmitter();
}
public get id(): string {
return this.id_;
return this.manifest.id;
}
public get enabled(): boolean {
return this.enabled_;
public get devMode(): boolean {
return this.devMode_;
}
public set enabled(v: boolean) {
this.enabled_ = v;
public set devMode(v: boolean) {
this.devMode_ = v;
}
public get manifest(): PluginManifest {
@@ -91,7 +89,7 @@ export default class Plugin {
this.contentScripts_[type].push({ id, path: absolutePath });
this.logger_.debug(`Plugin: ${this.id}: Registered content script: ${type}: ${id}: ${absolutePath}`);
logger.debug(`"${this.id}": Registered content script: ${type}: ${id}: ${absolutePath}`);
this.dispatch_({
type: 'PLUGIN_CONTENT_SCRIPTS_ADD',
@@ -119,7 +117,7 @@ export default class Plugin {
}
public deprecationNotice(goneInVersion: string, message: string) {
this.logger_.warn(`Plugin: ${this.id}: DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
logger.warn(`"${this.id}": DEPRECATION NOTICE: ${message} This will stop working in version ${goneInVersion}.`);
}
}

View File

@@ -4,17 +4,45 @@ import Global from './api/Global';
import BasePluginRunner from './BasePluginRunner';
import BaseService from '../BaseService';
import shim from '../../shim';
import { rtrimSlashes } from '../../path-utils';
import { filename, dirname, rtrimSlashes, basename } from '../../path-utils';
import Setting from '../../models/Setting';
import Logger from '../../Logger';
const compareVersions = require('compare-versions');
const { filename, dirname } = require('../../path-utils');
const uslug = require('uslug');
const md5File = require('md5-file/promise');
interface Plugins {
const logger = Logger.create('PluginService');
// Plugin data is split into two:
//
// - First there's the service `plugins` property, which contains the
// plugin static data, as loaded from the plugin file or directory. For
// example, the plugin ID, the manifest, the script files, etc.
//
// - Secondly, there's the `PluginSettings` data, which is dynamic and is
// used for example to enable or disable a plugin. Its state is saved to
// the user's settings.
export interface Plugins {
[key: string]: Plugin;
}
export interface PluginSetting {
enabled: boolean;
deleted: boolean;
}
export function defaultPluginSetting(): PluginSetting {
return {
enabled: true,
deleted: false,
};
}
export interface PluginSettings {
[pluginId: string]: PluginSetting;
}
function makePluginId(source: string): string {
// https://www.npmjs.com/package/slug#options
return uslug(source).substr(0,32);
@@ -49,15 +77,42 @@ export default class PluginService extends BaseService {
return this.plugins_;
}
private setPluginAt(pluginId: string, plugin: Plugin) {
this.plugins_ = {
...this.plugins_,
[pluginId]: plugin,
};
}
private deletePluginAt(pluginId: string) {
if (!this.plugins_[pluginId]) return;
this.plugins_ = { ...this.plugins_ };
delete this.plugins_[pluginId];
}
public pluginById(id: string): Plugin {
if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`);
return this.plugins_[id];
}
// public allPluginIds(): string[] {
// return Object.keys(this.plugins_);
// }
public unserializePluginSettings(settings: any): PluginSettings {
const output = { ...settings };
for (const pluginId in output) {
output[pluginId] = {
...defaultPluginSetting(),
...output[pluginId],
};
}
return output;
}
public serializePluginSettings(settings: PluginSettings): any {
return JSON.stringify(settings);
}
private async parsePluginJsBundle(jsBundleString: string) {
const scriptText = jsBundleString;
@@ -146,7 +201,7 @@ export default class PluginService extends BaseService {
}
}
private async loadPluginFromPath(path: string): Promise<Plugin> {
public async loadPluginFromPath(path: string): Promise<Plugin> {
path = rtrimSlashes(path);
const fsDriver = shim.fsDriver();
@@ -161,7 +216,7 @@ export default class PluginService extends BaseService {
distPath = `${path}/dist`;
}
this.logger().info(`PluginService: Loading plugin from ${path}`);
logger.info(`Loading plugin from ${path}`);
const scriptText = await fsDriver.readFile(`${distPath}/index.js`);
const manifestText = await fsDriver.readFile(`${distPath}/manifest.json`);
@@ -189,28 +244,8 @@ export default class PluginService extends BaseService {
}
const manifest = manifestFromObject(manifestObj);
const pluginId = manifest.id;
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if (this.plugins_[pluginId]) throw new Error(`There is already a plugin with this ID: ${pluginId}`);
const plugin = new Plugin(pluginId, baseDir, manifest, scriptText, this.logger(), (action: any) => this.store_.dispatch(action));
if (compareVersions(this.appVersion_, manifest.app_min_version) < 0) {
this.logger().info(`PluginService: Plugin "${pluginId}" was disabled because it requires a newer version of Joplin.`, manifest);
plugin.enabled = false;
} else {
this.store_.dispatch({
type: 'PLUGIN_ADD',
plugin: {
id: pluginId,
views: {},
contentScripts: {},
},
});
}
const plugin = new Plugin(baseDir, manifest, scriptText, (action: any) => this.store_.dispatch(action));
for (const msg of deprecationNotices) {
plugin.deprecationNotice('1.5', msg);
@@ -219,49 +254,123 @@ export default class PluginService extends BaseService {
return plugin;
}
public async loadAndRunPlugins(pluginDirOrPaths: string | string[]) {
private pluginEnabled(settings: PluginSettings, pluginId: string): boolean {
if (!settings[pluginId]) return true;
return settings[pluginId].enabled !== false;
}
public async loadAndRunPlugins(pluginDirOrPaths: string | string[], settings: PluginSettings, devMode: boolean = false) {
let pluginPaths = [];
if (Array.isArray(pluginDirOrPaths)) {
pluginPaths = pluginDirOrPaths;
} else {
pluginPaths = (await shim.fsDriver().readDirStats(pluginDirOrPaths))
.filter((stat: any) => (stat.isDirectory() || stat.path.toLowerCase().endsWith('.js')))
.filter((stat: any) => {
if (stat.isDirectory()) return true;
if (stat.path.toLowerCase().endsWith('.js')) return true;
if (stat.path.toLowerCase().endsWith('.jpl')) return true;
return false;
})
.map((stat: any) => `${pluginDirOrPaths}/${stat.path}`);
}
for (const pluginPath of pluginPaths) {
if (pluginPath.indexOf('_') === 0) {
this.logger().info(`PluginService: Plugin name starts with "_" and has not been loaded: ${pluginPath}`);
logger.info(`Plugin name starts with "_" and has not been loaded: ${pluginPath}`);
continue;
}
try {
const plugin = await this.loadPluginFromPath(pluginPath);
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if (this.plugins_[plugin.id]) throw new Error(`There is already a plugin with this ID: ${plugin.id}`);
this.setPluginAt(plugin.id, plugin);
if (!this.pluginEnabled(settings, plugin.id)) {
logger.info(`Not running disabled plugin: "${plugin.id}"`);
continue;
}
plugin.devMode = devMode;
await this.runPlugin(plugin);
} catch (error) {
this.logger().error(`PluginService: Could not load plugin: ${pluginPath}`, error);
logger.error(`Could not load plugin: ${pluginPath}`, error);
}
}
}
public async runPlugin(plugin: Plugin) {
this.plugins_[plugin.id] = plugin;
const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_);
if (compareVersions(this.appVersion_, plugin.manifest.app_min_version) < 0) {
throw new Error(`Plugin "${plugin.id}" was disabled because it requires Joplin version ${plugin.manifest.app_min_version} and current version is ${this.appVersion_}.`);
} else {
this.store_.dispatch({
type: 'PLUGIN_ADD',
plugin: {
id: plugin.id,
views: {},
contentScripts: {},
},
});
}
const pluginApi = new Global(this.platformImplementation_, plugin, this.store_);
return this.runner_.run(plugin, pluginApi);
}
// public async handleDisabledPlugins() {
// const enabledPlugins = this.allPluginIds();
// const v = await this.kvStore_.value<string>('pluginService.lastEnabledPlugins');
// const lastEnabledPlugins = v ? JSON.parse(v) : [];
public async installPlugin(jplPath: string): Promise<Plugin> {
logger.info(`Installing plugin: "${jplPath}"`);
// const disabledPlugins = [];
// for (const id of lastEnabledPlugins) {
// if (!enabledPlugins.includes(id)) disabledPlugins.push(id);
// }
const destPath = `${Setting.value('pluginDir')}/${basename(jplPath)}`;
await shim.fsDriver().copy(jplPath, destPath);
const plugin = await this.loadPluginFromPath(destPath);
if (!this.plugins_[plugin.id]) this.setPluginAt(plugin.id, plugin);
return plugin;
}
// await this.kvStore_.setValue('pluginService.lastEnabledPlugins', JSON.stringify(enabledPlugins));
// }
private async pluginPath(pluginId: string) {
const stats = await shim.fsDriver().readDirStats(Setting.value('pluginDir'), { recursive: false });
for (const stat of stats) {
if (filename(stat.path) === pluginId) {
return `${Setting.value('pluginDir')}/${stat.path}`;
}
}
return null;
}
public async uninstallPlugin(pluginId: string) {
logger.info(`Uninstalling plugin: "${pluginId}"`);
const path = await this.pluginPath(pluginId);
if (!path) {
// Plugin might have already been deleted
logger.error(`Could not find plugin path to uninstall - nothing will be done: ${pluginId}`);
} else {
await shim.fsDriver().remove(path);
}
this.deletePluginAt(pluginId);
}
public async uninstallPlugins(settings: PluginSettings): Promise<PluginSettings> {
let newSettings = settings;
for (const pluginId in settings) {
if (settings[pluginId].deleted) {
await this.uninstallPlugin(pluginId);
newSettings = { ...settings };
delete newSettings[pluginId];
}
}
return newSettings;
}
}

View File

@@ -1,6 +1,5 @@
import Plugin from '../Plugin';
import Joplin from './Joplin';
import Logger from '../../../Logger';
/**
* @ignore
@@ -16,8 +15,8 @@ export default class Global {
private requireWhiteList_: string[] = null;
// private consoleWrapper_:any = null;
constructor(logger: Logger, implementation: any, plugin: Plugin, store: any) {
this.joplin_ = new Joplin(logger, implementation.joplin, plugin, store);
constructor(implementation: any, plugin: Plugin, store: any) {
this.joplin_ = new Joplin(implementation.joplin, plugin, store);
// this.consoleWrapper_ = this.createConsoleWrapper(plugin.id);
}

View File

@@ -7,7 +7,6 @@ import JoplinCommands from './JoplinCommands';
import JoplinViews from './JoplinViews';
import JoplinInterop from './JoplinInterop';
import JoplinSettings from './JoplinSettings';
import Logger from '../../../Logger';
/**
* This is the main entry point to the Joplin API. You can access various services using the provided accessors.
@@ -35,9 +34,9 @@ export default class Joplin {
private interop_: JoplinInterop = null;
private settings_: JoplinSettings = null;
constructor(logger: Logger, implementation: any, plugin: Plugin, store: any) {
constructor(implementation: any, plugin: Plugin, store: any) {
this.data_ = new JoplinData();
this.plugins_ = new JoplinPlugins(logger, plugin);
this.plugins_ = new JoplinPlugins(plugin);
this.workspace_ = new JoplinWorkspace(implementation.workspace, store);
this.filters_ = new JoplinFilters();
this.commands_ = new JoplinCommands();

View File

@@ -2,16 +2,16 @@ import Plugin from '../Plugin';
import Logger from '../../../Logger';
import { ContentScriptType, Script } from './types';
const logger = Logger.create('joplin.plugins');
/**
* This class provides access to plugin-related features.
*/
export default class JoplinPlugins {
private logger: Logger;
private plugin: Plugin;
public constructor(logger: Logger, plugin: Plugin) {
this.logger = logger;
public constructor(plugin: Plugin) {
this.plugin = plugin;
}
@@ -31,7 +31,7 @@ export default class JoplinPlugins {
if (script.onStart) {
const startTime = Date.now();
this.logger.info(`Starting plugin: ${this.plugin.id}`);
logger.info(`Starting plugin: ${this.plugin.id}`);
// We don't use `await` when calling onStart because the plugin might be awaiting
// in that call too (for example, when opening a dialog on startup) so we don't
@@ -42,9 +42,9 @@ export default class JoplinPlugins {
// be handled correctly by loggers, etc.
const newError: Error = new Error(error.message);
newError.stack = error.stack;
this.logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
logger.error(`Uncaught exception in plugin "${this.plugin.id}":`, newError);
}).then(() => {
this.logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
logger.info(`Finished running onStart handler: ${this.plugin.id} (Took ${Date.now() - startTime}ms)`);
this.plugin.emit('started');
});
}

View File

@@ -51,7 +51,7 @@ const gunzipFile = function(source, destination) {
});
};
function shimInit(sharp = null, keytar = null) {
function shimInit(sharp = null, keytar = null, React = null) {
keytar = (shim.isWindows() || shim.isMac()) && !shim.isPortable() ? keytar : null;
shim.fsDriver = () => {
@@ -67,6 +67,12 @@ function shimInit(sharp = null, keytar = null) {
return shim.fsDriver_;
};
if (React) {
shim.react = () => {
return React;
};
}
shim.randomBytes = async count => {
const buffer = require('crypto').randomBytes(count);
return Array.from(buffer);

View File

@@ -1,6 +1,21 @@
import { ResourceEntity } from './services/database/types';
let isTestingEnv_ = false;
// We need to ensure that there's only one instance of React being used by
// all the packages. In particular, the lib might need React to define
// generic hooks, but it shouldn't have React in its dependencies as that
// would cause the following error:
//
// https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react
//
// So instead, the **applications** include React as a dependency, then
// pass it to any other packages using the shim. Essentially, only one
// package should require React, and in our case that should be one of the
// applications (app-desktop, app-mobile, etc.) since we are sure they
// won't be dependency to other packages (unlike the lib which can be
// included anywhere).
let react_: any = null;
const shim = {

View File

@@ -68,9 +68,9 @@ function slugify(s: string): string {
const inMemoryCache = new InMemoryCache(20);
export interface ExtraRendererRule {
id: string,
module: any,
assetPath: string,
id: string;
module: any;
assetPath: string;
}
export interface Options {

View File

@@ -144,22 +144,6 @@
"resolve": "^1.12.0"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@@ -222,12 +206,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
@@ -568,26 +546,6 @@
"universalify": "^0.1.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -648,43 +606,11 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"jasmine": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.5.0.tgz",
"integrity": "sha512-DYypSryORqzsGoMazemIHUfMkXM7I7easFaxAvNM3Mr6Xz3Fy36TupTrAOxZWN8MVKEU5xECv22J4tUQf3uBzQ==",
"dev": true,
"requires": {
"glob": "^7.1.4",
"jasmine-core": "~3.5.0"
}
},
"jasmine-core": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz",
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -894,15 +820,6 @@
"try-to-catch": "^1.0.2"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"moment-mini": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.24.0.tgz",
@@ -929,15 +846,6 @@
"lodash.toarray": "^4.4.0"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
@@ -946,12 +854,6 @@
"no-case": "^2.2.0"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
@@ -1089,12 +991,6 @@
"node-emoji": "^1.10.0",
"unorm": ">= 1.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}

View File

@@ -18,7 +18,6 @@
"license": "MIT",
"devDependencies": {
"@types/node": "^14.14.6",
"jasmine": "^3.5.0",
"typescript": "^4.0.5"
},
"dependencies": {

View File

@@ -2,8 +2,10 @@
// Dependencies:
//
// sudo apt install gettext
// sudo apt install translate-toolkit
// sudo apt install gettext sudo apt install translate-toolkit
//
// gettext v21+ is required as versions before that have bugs when parsing
// JavaScript template strings which means we would lose translations.
const rootDir = `${__dirname}/../..`;

View File

@@ -26,8 +26,8 @@ With plugins you can directly modify Joplin by adding new features to the applic
- Create a module to export or import data into Joplin
- Define new settings and setting sections, and get/set them from the plugin
- Create a new Markdown plugin to render custom markup.
- Create an editor plugin to modify low-level the behaviour of the Markdown editor (CodeMirror)
- Create an editor plugin to modify, at a low-level, the behaviour of the Markdown editor (CodeMirror)
To get started with the plugin API, check the [Get Started](https://github.com/laurent22/joplin/blob/dev/readme/api/get_started/plugins.md) page or have a look at the [TOC tutorial](https://github.com/laurent22/joplin/blob/dev/readme/api/tutorials/toc_plugin.md).
Once you are familiar with the API, you can have a look at the [plugin API reference](https://github.com/laurent22/joplin/blob/dev/readme/api/get_started/plugins.md) for a detailed documentation about each supported feature.
Once you are familiar with the API, you can have a look at the [plugin API reference](https://joplinapp.org/api/references/plugin_api/classes/joplin.html) for a detailed documentation about each supported feature.