Merge branch 'master' of github.com:laurent22/joplin

pull/3327/head
Laurent Cozic 2020-06-04 07:55:13 +01:00
commit d9c266e3f1
234 changed files with 71387 additions and 70143 deletions

View File

@ -97,6 +97,13 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
ReactNativeClient/lib/services/ResourceEditWatcher.js
ReactNativeClient/lib/services/SettingUtils.js
ReactNativeClient/PluginAssetsLoader.js
ReactNativeClient/setUpQuickActions.js
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

7
.gitignore vendored
View File

@ -87,6 +87,13 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
ReactNativeClient/lib/services/ResourceEditWatcher.js
ReactNativeClient/lib/services/SettingUtils.js
ReactNativeClient/PluginAssetsLoader.js
ReactNativeClient/setUpQuickActions.js
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD

View File

@ -42,6 +42,8 @@ before_install:
# Silence apt-get update errors (for example when a module doesn't exist) since
# otherwise it will make the whole build fails, even though all we need is yarn.
# libsecret-1-dev is required for keytar - https://github.com/atom/node-keytar
- |
if [ "$TRAVIS_OS_NAME" == "osx" ]; then
HOMEBREW_NO_AUTO_UPDATE=1 brew install yarn
@ -51,6 +53,7 @@ before_install:
sudo apt-get update || true
sudo apt-get install -y yarn
sudo apt-get install -y gettext
sudo apt-get install -y libsecret-1-dev
fi
script:

View File

@ -29,6 +29,9 @@ const { shimInit } = require('lib/shim-init-node.js');
const { _ } = require('lib/locale.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const EncryptionService = require('lib/services/EncryptionService');
const envFromArgs = require('lib/envFromArgs');
const env = envFromArgs(process.argv);
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
@ -46,9 +49,11 @@ BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appId', `net.cozic.joplin${env === 'dev' ? 'dev' : ''}-cli`);
Setting.setConstant('appType', 'cli');
console.info(Setting.value('appId'));
shimInit();
const application = app();

View File

@ -3,6 +3,7 @@ const fs = require('fs-extra');
const utils = require('../Tools/gulp/utils');
const tasks = {
copyLib: require('../Tools/gulp/tasks/copyLib'),
tsc: require('../Tools/gulp/tasks/tsc'),
};
tasks.build = {
@ -45,5 +46,16 @@ tasks.buildTests = {
},
};
const buildTestSeries = [
tasks.buildTests.fn,
];
if (require('os').platform() === 'win32') {
gulp.task('copyLib', tasks.copyLib.fn);
gulp.task('tsc', tasks.tsc.fn);
buildTestSeries.push('copyLib');
buildTestSeries.push('tsc');
}
gulp.task('build', tasks.build.fn);
gulp.task('buildTests', tasks.buildTests.fn);
gulp.task('buildTests', gulp.series(...buildTestSeries));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -38,42 +38,42 @@ locales['tr_TR'] = require('./tr_TR.json');
locales['vi'] = require('./vi.json');
locales['zh_CN'] = require('./zh_CN.json');
locales['zh_TW'] = require('./zh_TW.json');
stats['ar'] = {"percentDone":86};
stats['ar'] = {"percentDone":85};
stats['eu'] = {"percentDone":36};
stats['bs_BA'] = {"percentDone":80};
stats['bs_BA'] = {"percentDone":79};
stats['bg_BG'] = {"percentDone":71};
stats['ca'] = {"percentDone":57};
stats['ca'] = {"percentDone":56};
stats['hr_HR'] = {"percentDone":30};
stats['cs_CZ'] = {"percentDone":88};
stats['da_DK'] = {"percentDone":79};
stats['de_DE'] = {"percentDone":96};
stats['et_EE'] = {"percentDone":71};
stats['et_EE'] = {"percentDone":70};
stats['en_GB'] = {"percentDone":100};
stats['en_US'] = {"percentDone":100};
stats['es_ES'] = {"percentDone":88};
stats['eo'] = {"percentDone":41};
stats['fr_FR'] = {"percentDone":92};
stats['es_ES'] = {"percentDone":95};
stats['eo'] = {"percentDone":40};
stats['fr_FR'] = {"percentDone":100};
stats['gl_ES'] = {"percentDone":46};
stats['id_ID'] = {"percentDone":97};
stats['it_IT'] = {"percentDone":97};
stats['nl_NL'] = {"percentDone":91};
stats['id_ID'] = {"percentDone":98};
stats['it_IT'] = {"percentDone":95};
stats['nl_BE'] = {"percentDone":36};
stats['nb_NO'] = {"percentDone":95};
stats['fa'] = {"percentDone":36};
stats['pl_PL'] = {"percentDone":90};
stats['pt_PT'] = {"percentDone":95};
stats['pt_BR'] = {"percentDone":97};
stats['nl_NL'] = {"percentDone":90};
stats['nb_NO'] = {"percentDone":93};
stats['fa'] = {"percentDone":35};
stats['pl_PL'] = {"percentDone":89};
stats['pt_PT'] = {"percentDone":94};
stats['pt_BR'] = {"percentDone":95};
stats['ro'] = {"percentDone":36};
stats['sl_SI'] = {"percentDone":46};
stats['sv'] = {"percentDone":76};
stats['th_TH'] = {"percentDone":57};
stats['vi'] = {"percentDone":92};
stats['tr_TR'] = {"percentDone":97};
stats['el_GR'] = {"percentDone":97};
stats['ru_RU'] = {"percentDone":94};
stats['sr_RS'] = {"percentDone":77};
stats['zh_CN'] = {"percentDone":95};
stats['zh_TW'] = {"percentDone":95};
stats['ja_JP'] = {"percentDone":97};
stats['ko'] = {"percentDone":93};
stats['sl_SI'] = {"percentDone":45};
stats['sv'] = {"percentDone":75};
stats['th_TH'] = {"percentDone":56};
stats['vi'] = {"percentDone":91};
stats['tr_TR'] = {"percentDone":95};
stats['el_GR'] = {"percentDone":95};
stats['ru_RU'] = {"percentDone":93};
stats['sr_RS'] = {"percentDone":76};
stats['zh_CN'] = {"percentDone":93};
stats['zh_TW'] = {"percentDone":94};
stats['ja_JP'] = {"percentDone":98};
stats['ko'] = {"percentDone":92};
module.exports = { locales: locales, stats: stats };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3859,6 +3859,22 @@
}
}
},
"keytar": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-5.6.0.tgz",
"integrity": "sha512-ueulhshHSGoryfRXaIvTj0BV1yB0KddBGhGoqCxSN9LR1Ks1GKuuCdVhF+2/YOs5fMl6MlTI9On1a4DHDXoTow==",
"requires": {
"nan": "2.14.1",
"prebuild-install": "5.3.3"
},
"dependencies": {
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
}
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",

View File

@ -4,7 +4,7 @@
"license": "MIT",
"author": "Laurent Cozic",
"scripts": {
"test": "gulp buildTests -L && jasmine --config=tests/support/jasmine.json",
"test": "gulp buildTests -L && node node_modules/jasmine/bin/jasmine.js --config=tests/support/jasmine.json",
"postinstall": "npm run build && patch-package --patch-dir ../patches",
"build": "gulp build",
"start": "gulp build -L && node 'build/main.js' --stack-trace-enabled --log-level debug --env dev"
@ -60,6 +60,7 @@
"json-stringify-safe": "^5.0.1",
"jssha": "^2.3.0",
"katex": "^0.11.1",
"keytar": "^5.4.0",
"levenshtein": "^1.0.5",
"markdown-it": "^10.0.0",
"markdown-it-abbr": "^1.0.4",

View File

@ -223,6 +223,8 @@ describe('models_Note', function() {
const resourceDir = Setting.value('resourceDir');
const r1 = await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`);
const r2 = await shim.createResourceFromPath(`${__dirname}/../tests/support/photo.jpg`);
const t1 = r1.updated_time;
const t2 = r2.updated_time;
const testCases = [
[
@ -248,12 +250,12 @@ describe('models_Note', function() {
[
true,
`![](:/${r1.id})`,
`![](file://${resourceDir}/${r1.id}.jpg)`,
`![](file://${resourceDir}/${r1.id}.jpg?t=${t1})`,
],
[
true,
`![](:/${r1.id}) ![](:/${r1.id}) ![](:/${r2.id})`,
`![](file://${resourceDir}/${r1.id}.jpg) ![](file://${resourceDir}/${r1.id}.jpg) ![](file://${resourceDir}/${r2.id}.jpg)`,
`![](file://${resourceDir}/${r1.id}.jpg?t=${t1}) ![](file://${resourceDir}/${r1.id}.jpg?t=${t1}) ![](file://${resourceDir}/${r2.id}.jpg?t=${t2})`,
],
];

View File

@ -4,6 +4,7 @@ require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const { shim } = require('lib/shim');
const Setting = require('lib/models/Setting.js');
process.on('unhandledRejection', (reason, p) => {
@ -13,6 +14,8 @@ process.on('unhandledRejection', (reason, p) => {
describe('models_Setting', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});

View File

@ -257,22 +257,29 @@ describe('services_SearchEngine', function() {
it('should support queries with Chinese characters', asyncTest(async () => {
let rows;
const n1 = await Note.save({ title: '我是法国人' });
const n1 = await Note.save({ title: '我是法国人', body: '中文测试' });
await engine.syncTables();
expect((await engine.search('我')).length).toBe(1);
expect((await engine.search('法国人')).length).toBe(1);
expect((await engine.search('法国人*'))[0].fields.sort()).toEqual(['body', 'title']); // usually assume that keyword was matched in body
expect((await engine.search('测试')).length).toBe(1);
expect((await engine.search('测试'))[0].fields).toEqual(['body']);
expect((await engine.search('测试*'))[0].fields).toEqual(['body']);
}));
it('should support queries with Japanese characters', asyncTest(async () => {
let rows;
const n1 = await Note.save({ title: '私は日本語を話すことができません' });
const n1 = await Note.save({ title: '私は日本語を話すことができません', body: 'テスト' });
await engine.syncTables();
expect((await engine.search('日本')).length).toBe(1);
expect((await engine.search('できません')).length).toBe(1);
expect((await engine.search('できません*'))[0].fields.sort()).toEqual(['body', 'title']); // usually assume that keyword was matched in body
expect((await engine.search('テスト'))[0].fields.sort()).toEqual(['body']);
}));
it('should support queries with Korean characters', asyncTest(async () => {
@ -302,10 +309,15 @@ describe('services_SearchEngine', function() {
await engine.syncTables();
expect((await engine.search('title:你好*')).length).toBe(1);
expect((await engine.search('title:你好*'))[0].fields).toEqual(['title']);
expect((await engine.search('body:法国人')).length).toBe(1);
expect((await engine.search('body:法国人'))[0].fields).toEqual(['body']);
expect((await engine.search('body:你好')).length).toBe(0);
expect((await engine.search('title:你好 body:法国人')).length).toBe(1);
expect((await engine.search('title:你好 body:法国人'))[0].fields.sort()).toEqual(['body', 'title']);
expect((await engine.search('title:你好 body:bla')).length).toBe(0);
expect((await engine.search('title:你好 我是')).length).toBe(1);
expect((await engine.search('title:你好 我是'))[0].fields.sort()).toEqual(['body', 'title']);
expect((await engine.search('title:bla 我是')).length).toBe(0);
// For non-alpha char, only the first field is looked at, the following ones are ignored

View File

@ -0,0 +1,60 @@
/* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname);
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const { shim } = require('lib/shim');
const Setting = require('lib/models/Setting');
const KeychainService = require('lib/services/keychain/KeychainService').default;
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function describeIfCompatible(name, fn) {
if (['win32', 'darwin'].includes(shim.platformName())) {
return describe(name, fn);
}
}
describeIfCompatible('services_KeychainService', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1, { keychainEnabled: true });
await switchClient(1, { keychainEnabled: true });
await Setting.deleteKeychainPasswords();
done();
});
afterEach(async (done) => {
await Setting.deleteKeychainPasswords();
done();
});
it('should be enabled on macOS and Windows', asyncTest(async () => {
expect(Setting.value('keychain.supported')).toBe(1);
}));
it('should set, get and delete passwords', asyncTest(async () => {
const service = KeychainService.instance();
const isSet = await service.setPassword('zz_testunit', 'password');
expect(isSet).toBe(true);
const password = await service.password('zz_testunit');
expect(password).toBe('password');
await service.deletePassword('zz_testunit');
expect(await service.password('zz_testunit')).toBe(null);
}));
it('should save and load secure settings', asyncTest(async () => {
Setting.setObjectKey('encryption.passwordCache', 'testing', '123456');
await Setting.saveAll();
await Setting.load();
const passwords = Setting.value('encryption.passwordCache');
expect(passwords.testing).toBe('123456');
}));
});

View File

@ -3,7 +3,7 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { setupDatabase, allSyncTargetItemsEncrypted, kvStore, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
const { setupDatabase, allSyncTargetItemsEncrypted, tempFilePath, resourceFetcher, kvStore, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
const { shim } = require('lib/shim.js');
const fs = require('fs-extra');
const Folder = require('lib/models/Folder.js');
@ -991,6 +991,145 @@ describe('synchronizer', function() {
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
}));
it('should sync resource blob changes', asyncTest(async () => {
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, tempFile);
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
let resource1_2 = (await Resource.all())[0];
const modFile = tempFilePath('txt');
await shim.fsDriver().writeFile(modFile, '1234 MOD', 'utf8');
await Resource.updateResourceBlobContent(resource1_2.id, modFile);
const originalSize = resource1_2.size;
resource1_2 = (await Resource.all())[0];
const newSize = resource1_2.size;
expect(originalSize).toBe(4);
expect(newSize).toBe(8);
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
const resource1_1 = (await Resource.all())[0];
expect(resource1_1.size).toBe(newSize);
expect(await Resource.resourceBlobContent(resource1_1.id, 'utf8')).toBe('1234 MOD');
}));
it('should handle resource conflicts', asyncTest(async () => {
{
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, tempFile);
await synchronizer().start();
}
await switchClient(2);
{
await synchronizer().start();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
const resource = (await Resource.all())[0];
const modFile2 = tempFilePath('txt');
await shim.fsDriver().writeFile(modFile2, '1234 MOD 2', 'utf8');
await Resource.updateResourceBlobContent(resource.id, modFile2);
await synchronizer().start();
}
await switchClient(1);
{
// Going to modify a resource without syncing first, which will cause a conflict
const resource = (await Resource.all())[0];
const modFile1 = tempFilePath('txt');
await shim.fsDriver().writeFile(modFile1, '1234 MOD 1', 'utf8');
await Resource.updateResourceBlobContent(resource.id, modFile1);
await synchronizer().start(); // CONFLICT
// If we try to read the resource content now, it should throw because the local
// content has been moved to the conflict notebook, and the new local content
// has not been downloaded yet.
await checkThrowAsync(async () => await Resource.resourceBlobContent(resource.id));
// Now download resources, and our local content would have been overwritten by
// the content from client 2
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
const localContent = await Resource.resourceBlobContent(resource.id, 'utf8');
expect(localContent).toBe('1234 MOD 2');
// Check that the Conflict note has been generated, with the conflict resource
// attached to it, and check that it has the original content.
const allNotes = await Note.all();
expect(allNotes.length).toBe(2);
const conflictNote = allNotes.find((v) => {
return !!v.is_conflict;
});
expect(!!conflictNote).toBe(true);
const resourceIds = await Note.linkedResourceIds(conflictNote.body);
expect(resourceIds.length).toBe(1);
const conflictContent = await Resource.resourceBlobContent(resourceIds[0], 'utf8');
expect(conflictContent).toBe('1234 MOD 1');
}
}));
it('should handle resource conflicts if a resource is changed locally but deleted remotely', asyncTest(async () => {
{
const tempFile = tempFilePath('txt');
await shim.fsDriver().writeFile(tempFile, '1234', 'utf8');
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, tempFile);
await synchronizer().start();
}
await switchClient(2);
{
await synchronizer().start();
await resourceFetcher().start();
await resourceFetcher().waitForAllFinished();
}
await switchClient(1);
{
const resource = (await Resource.all())[0];
await Resource.delete(resource.id);
await synchronizer().start();
}
await switchClient(2);
{
const originalResource = (await Resource.all())[0];
await Resource.save({ id: originalResource.id, title: 'modified resource' });
await synchronizer().start(); // CONFLICT
const deletedResource = await Resource.load(originalResource.id);
expect(!deletedResource).toBe(true);
const allResources = await Resource.all();
expect(allResources.length).toBe(1);
const conflictResource = allResources[0];
expect(originalResource.id).not.toBe(conflictResource.id);
expect(conflictResource.title).toBe('modified resource');
}
}));
it('should upload decrypted items to sync target after encryption disabled', asyncTest(async () => {
Setting.setValue('encryption.enabled', true);
const masterKey = await loadEncryptionMasterKey();

View File

@ -37,9 +37,14 @@ const EncryptionService = require('lib/services/EncryptionService.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const ResourceService = require('lib/services/ResourceService.js');
const RevisionService = require('lib/services/RevisionService.js');
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const KvStore = require('lib/services/KvStore.js');
const WebDavApi = require('lib/WebDavApi');
const DropboxApi = require('lib/DropboxApi');
const { loadKeychainServiceAndSettings } = require('lib/services/SettingUtils');
const KeychainServiceDriver = require('lib/services/keychain/KeychainServiceDriver.node').default;
const KeychainServiceDriverDummy = require('lib/services/keychain/KeychainServiceDriver.dummy').default;
const md5 = require('md5');
const databases_ = [];
const synchronizers_ = [];
@ -47,6 +52,7 @@ const encryptionServices_ = [];
const revisionServices_ = [];
const decryptionWorkers_ = [];
const resourceServices_ = [];
const resourceFetchers_ = [];
const kvStores_ = [];
let fileApi_ = null;
let currentClient_ = 1;
@ -106,7 +112,7 @@ BaseItem.loadClass('NoteTag', NoteTag);
BaseItem.loadClass('MasterKey', MasterKey);
BaseItem.loadClass('Revision', Revision);
Setting.setConstant('appId', 'net.cozic.joplin-cli');
Setting.setConstant('appId', 'net.cozic.joplintest-cli');
Setting.setConstant('appType', 'cli');
Setting.setConstant('tempDir', tempDir);
@ -130,7 +136,9 @@ function currentClientId() {
return currentClient_;
}
async function switchClient(id) {
async function switchClient(id, options = null) {
options = Object.assign({}, { keychainEnabled: false }, options);
if (!databases_[id]) throw new Error(`Call setupDatabaseAndSynchronizer(${id}) first!!`);
await time.msleep(sleepTime); // Always leave a little time so that updated_time properties don't overlap
@ -146,9 +154,8 @@ async function switchClient(id) {
Setting.setConstant('resourceDirName', resourceDirName(id));
Setting.setConstant('resourceDir', resourceDir(id));
await Setting.load();
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
Setting.setValue('sync.wipeOutFailSafe', false); // To keep things simple, always disable fail-safe unless explicitely set in the test itself
}
@ -183,7 +190,9 @@ async function clearDatabase(id = null) {
await databases_[id].transactionExecBatch(queries);
}
async function setupDatabase(id = null) {
async function setupDatabase(id = null, options = null) {
options = Object.assign({}, { keychainEnabled: false }, options);
if (id === null) id = currentClient_;
Setting.cancelScheduleSave();
@ -192,8 +201,7 @@ async function setupDatabase(id = null) {
if (databases_[id]) {
BaseModel.setDb(databases_[id]);
await clearDatabase(id);
await Setting.load();
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
return;
}
@ -210,8 +218,7 @@ async function setupDatabase(id = null) {
await databases_[id].open({ name: filePath });
BaseModel.setDb(databases_[id]);
await Setting.load();
if (!Setting.value('clientId')) Setting.setValue('clientId', uuid.create());
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
}
function resourceDirName(id = null) {
@ -224,12 +231,12 @@ function resourceDir(id = null) {
return `${__dirname}/data/${resourceDirName(id)}`;
}
async function setupDatabaseAndSynchronizer(id = null) {
async function setupDatabaseAndSynchronizer(id = null, options = null) {
if (id === null) id = currentClient_;
BaseService.logger_ = logger;
await setupDatabase(id);
await setupDatabase(id, options);
EncryptionService.instance_ = null;
DecryptionWorker.instance_ = null;
@ -250,6 +257,7 @@ async function setupDatabaseAndSynchronizer(id = null) {
decryptionWorkers_[id] = new DecryptionWorker();
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
resourceServices_[id] = new ResourceService();
resourceFetchers_[id] = new ResourceFetcher(() => { return synchronizers_[id].api(); });
kvStores_[id] = new KvStore();
await fileApi().clearRoot();
@ -294,6 +302,11 @@ function resourceService(id = null) {
return resourceServices_[id];
}
function resourceFetcher(id = null) {
if (id === null) id = currentClient_;
return resourceFetchers_[id];
}
async function loadEncryptionMasterKey(id = null, useExisting = false) {
const service = encryptionService(id);
@ -478,6 +491,10 @@ async function createNTestTags(n) {
return tags;
}
function tempFilePath(ext) {
return `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}.${ext}`;
}
// Application for feature integration testing
class TestApp extends BaseApplication {
constructor(hasGui = true) {
@ -546,4 +563,4 @@ class TestApp extends BaseApplication {
}
}
module.exports = { kvStore, resourceService, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
module.exports = { kvStore, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

@ -23,6 +23,7 @@ const InteropServiceHelper = require('./InteropServiceHelper.js');
const ResourceService = require('lib/services/ResourceService');
const ClipperServer = require('lib/ClipperServer');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const ResourceEditWatcher = require('lib/services/ResourceEditWatcher').default;
const { bridge } = require('electron').remote.require('./bridge');
const { shell, webFrame, clipboard } = require('electron');
const Menu = bridge().Menu;
@ -685,6 +686,7 @@ class Application extends BaseApplication {
_('Client ID: %s', Setting.value('clientId')),
_('Sync Version: %s', Setting.value('syncVersion')),
_('Profile Version: %s', reg.db().version()),
_('Keychain Supported: %s', Setting.value('keychain.supported') >= 1 ? _('Yes') : _('No')),
];
if (gitInfo) {
message.push(`\n${gitInfo}`);
@ -1505,6 +1507,8 @@ class Application extends BaseApplication {
ExternalEditWatcher.instance().setLogger(reg.logger());
ExternalEditWatcher.instance().dispatch = this.store().dispatch;
ResourceEditWatcher.instance().initialize(reg.logger(), this.store().dispatch);
RevisionService.instance().runInBackground();
this.updateMenuItemStates();

View File

@ -124,7 +124,7 @@ export default function NoteContentPropertiesDialog(props:NoteContentPropertiesD
return (
<div style={theme.dialogModalLayer}>
<div style={theme.dialogBox}>
<div style={dialogBoxHeadingStyle}>{_('Content properties')}</div>
<div style={dialogBoxHeadingStyle}>{_('Statistics')}</div>
<table>
<thead>
{tableHeader}

View File

@ -7,7 +7,6 @@ import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceH
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
import { textOffsetToCursorPosition, useScrollHandler, useRootWidth, usePrevious, lineLeftSpaces, selectionRange, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText } from './utils';
import useListIdent from './utils/useListIdent';
import useFocus from './utils/useFocus';
import Toolbar from './Toolbar';
import styles_ from './styles';
import { RenderedBody, defaultRenderedBody } from './utils/types';
@ -553,8 +552,6 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
}
}, [props.searchMarkers, renderedBody]);
const { focused, onBlur, onFocus } = useFocus();
const cellEditorStyle = useMemo(() => {
const output = { ...styles.cellEditor };
if (!props.visiblePanes.includes('editor')) {
@ -590,6 +587,8 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
return output;
}, [styles.cellViewer, props.visiblePanes]);
const editorReadOnly = props.visiblePanes.indexOf('editor') < 0;
function renderEditor() {
// Need to hard-code the editor width, otherwise various bugs pops up
let width = 0;
@ -603,13 +602,11 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
value={props.content}
mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'text' : 'markdown'}
theme={styles.editor.editorTheme}
onFocus={onFocus}
onBlur={onBlur}
style={styles.editor}
width={`${width}px`}
fontSize={styles.editor.fontSize}
showGutter={false}
readOnly={props.visiblePanes.indexOf('editor') < 0}
readOnly={editorReadOnly}
name="note-editor"
wrapEnabled={true}
onScroll={editor_scroll}
@ -654,7 +651,7 @@ function AceEditor(props: NoteBodyEditorProps, ref: any) {
<Toolbar
theme={props.theme}
dispatch={props.dispatch}
disabled={!focused}
disabled={editorReadOnly}
/>
{props.noteToolbar}
</div>

View File

@ -1,15 +0,0 @@
import { useState, useCallback } from 'react';
export default function useFocus() {
const [focused, setFocused] = useState(false);
const onFocus = useCallback(() => {
setFocused(true);
}, []);
const onBlur = useCallback(() => {
setFocused(false);
}, []);
return { focused, onFocus, onBlur };
}

View File

@ -158,7 +158,10 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
const markupToHtml = useRef(null);
markupToHtml.current = props.markupToHtml;
const lastOnChangeEventContent = useRef<string>('');
const lastOnChangeEventInfo = useRef<any>({
content: null,
resourceInfos: null,
});
const rootIdRef = useRef<string>(`tinymce-${Date.now()}${Math.round(Math.random() * 10000)}`);
const editorRef = useRef<any>(null);
@ -761,10 +764,15 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
let cancelled = false;
const loadContent = async () => {
if (lastOnChangeEventContent.current !== props.content) {
if (lastOnChangeEventInfo.current.content !== props.content || lastOnChangeEventInfo.current.resourceInfos !== props.resourceInfos) {
const result = await props.markupToHtml(props.contentMarkupLanguage, props.content, markupRenderOptions({ resourceInfos: props.resourceInfos }));
if (cancelled) return;
lastOnChangeEventContent.current = props.content;
lastOnChangeEventInfo.current = {
content: props.content,
resourceInfos: props.resourceInfos,
};
editor.setContent(result.html);
}
@ -859,7 +867,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
if (!editor) return;
lastOnChangeEventContent.current = contentMd;
lastOnChangeEventInfo.current.content = contentMd;
props_onChangeRef.current({
changeId: changeId,

View File

@ -28,6 +28,7 @@ const { _ } = require('lib/locale');
const Note = require('lib/models/Note.js');
const { bridge } = require('electron').remote.require('./bridge');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
import ResourceEditWatcher from '../../lib/services/ResourceEditWatcher';
const eventManager = require('../../eventManager');
const NoteRevisionViewer = require('../NoteRevisionViewer.min');
const TagList = require('../TagList.min.js');
@ -172,6 +173,8 @@ function NoteEditor(props: NoteEditorProps) {
type: props.selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
value: props.selectedNoteHash ? props.selectedNoteHash : props.lastEditorScrollPercents[props.noteId] || 0,
});
ResourceEditWatcher.instance().stopWatchingAll();
}, [formNote.id, previousNoteId]);
const onFieldChange = useCallback((field: string, value: any, changeId = 0) => {

View File

@ -7,6 +7,8 @@ const { clipboard } = require('electron');
const { toSystemSlashes } = require('lib/path-utils');
const { _ } = require('lib/locale');
import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher';
export enum ContextMenuItemType {
None = '',
Image = 'image',
@ -42,9 +44,12 @@ export function menuItems():ContextMenuItems {
open: {
label: _('Open...'),
onAction: async (options:ContextMenuOptions) => {
const { resourcePath } = await resourceInfo(options);
const ok = bridge().openExternal(`file://${resourcePath}`);
if (!ok) bridge().showErrorMessageBox(_('This file could not be opened: %s', resourcePath));
try {
await ResourceEditWatcher.instance().openAndWatch(options.resourceId);
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(error.message);
}
},
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},

View File

@ -11,6 +11,7 @@ const Setting = require('lib/models/Setting');
const { reg } = require('lib/registry.js');
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const ResourceEditWatcher = require('lib/services/ResourceEditWatcher.js').default;
export interface OnLoadEvent {
formNote: FormNote,
@ -30,12 +31,14 @@ function installResourceChangeHandler(onResourceChangeHandler: Function) {
ResourceFetcher.instance().on('downloadComplete', onResourceChangeHandler);
ResourceFetcher.instance().on('downloadStarted', onResourceChangeHandler);
DecryptionWorker.instance().on('resourceDecrypted', onResourceChangeHandler);
ResourceEditWatcher.instance().on('resourceChange', onResourceChangeHandler);
}
function uninstallResourceChangeHandler(onResourceChangeHandler: Function) {
ResourceFetcher.instance().off('downloadComplete', onResourceChangeHandler);
ResourceFetcher.instance().off('downloadStarted', onResourceChangeHandler);
DecryptionWorker.instance().off('resourceDecrypted', onResourceChangeHandler);
ResourceEditWatcher.instance().off('resourceChange', onResourceChangeHandler);
}
export default function useFormNote(dependencies:HookDependencies) {

View File

@ -10,6 +10,7 @@ const { urlDecode } = require('lib/string-utils');
const urlUtils = require('lib/urlUtils');
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const { reg } = require('lib/registry.js');
import ResourceEditWatcher from '../../../lib/services/ResourceEditWatcher';
export default function useMessageHandler(scrollWhenReady:any, setScrollWhenReady:Function, editorRef:any, setLocalSearchResultCount:Function, dispatch:Function, formNote:FormNote) {
return useCallback(async (event: any) => {
@ -17,7 +18,7 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
const args = event.args;
const arg0 = args && args.length >= 1 ? args[0] : null;
if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, args);
if (msg !== 'percentScroll') console.info(`Got ipc-message: ${msg}`, arg0);
if (msg.indexOf('error:') === 0) {
const s = msg.split(':');
@ -60,8 +61,13 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
}
return;
}
const filePath = Resource.fullPath(item);
bridge().openItem(filePath);
try {
await ResourceEditWatcher.instance().openAndWatch(item.id);
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(error.message);
}
} else if (item.type_ === BaseModel.TYPE_NOTE) {
dispatch({
type: 'FOLDER_AND_NOTE_SELECT',

View File

@ -57,7 +57,7 @@ function useToolbarItems(props:NoteToolbarProps) {
});
toolbarItems.push({
tooltip: _('Front'),
tooltip: _('Forward'),
iconName: 'fa-arrow-right',
enabled: (forwardHistoryNotes.length > 0),
onClick: () => {

Some files were not shown because too many files have changed in this diff Show More