You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-05 20:56:22 +02:00
Compare commits
22 Commits
cli-v1.2.1
...
v1.2.4
Author | SHA1 | Date | |
---|---|---|---|
|
00057da17d | ||
|
0a05464013 | ||
|
9ebb574059 | ||
|
d29c3c2466 | ||
|
a71f1c19ec | ||
|
485921d879 | ||
|
15de7572c0 | ||
|
09f41dd50e | ||
|
7b8ee467a0 | ||
|
99a496d684 | ||
|
f43ee123d8 | ||
|
f42fb1b871 | ||
|
cf2442c5b2 | ||
|
e0e4735b03 | ||
|
8bd58c9608 | ||
|
215a725ded | ||
|
12c0a05af0 | ||
|
a7fa119041 | ||
|
7fb52b8b0e | ||
|
3e86ae4a82 | ||
|
947d81d96d | ||
|
6ca640d2ed |
@@ -119,6 +119,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||
@@ -145,6 +146,7 @@ ElectronClient/gui/NoteList/NoteList.js
|
||||
ElectronClient/gui/NoteListControls/commands/focusSearch.js
|
||||
ElectronClient/gui/NoteListControls/NoteListControls.js
|
||||
ElectronClient/gui/NoteListItem.js
|
||||
ElectronClient/gui/NoteTextViewer.js
|
||||
ElectronClient/gui/NoteToolbar/NoteToolbar.js
|
||||
ElectronClient/gui/OneDriveLoginScreen.js
|
||||
ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.js
|
||||
@@ -176,6 +178,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
|
||||
ReactNativeClient/lib/hooks/useEffectDebugger.js
|
||||
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
|
||||
ReactNativeClient/lib/hooks/usePrevious.js
|
||||
ReactNativeClient/lib/hooks/usePropsDebugger.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
||||
@@ -183,6 +186,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/lib/ntpDate.js
|
||||
ReactNativeClient/lib/services/CommandService.js
|
||||
ReactNativeClient/lib/services/debug/populateDatabase.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -112,6 +112,7 @@ ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useCursorUtils.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearch.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useJoplinMode.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useKeymap.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useLineSorting.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useListIdent.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.js
|
||||
@@ -138,6 +139,7 @@ ElectronClient/gui/NoteList/NoteList.js
|
||||
ElectronClient/gui/NoteListControls/commands/focusSearch.js
|
||||
ElectronClient/gui/NoteListControls/NoteListControls.js
|
||||
ElectronClient/gui/NoteListItem.js
|
||||
ElectronClient/gui/NoteTextViewer.js
|
||||
ElectronClient/gui/NoteToolbar/NoteToolbar.js
|
||||
ElectronClient/gui/OneDriveLoginScreen.js
|
||||
ElectronClient/gui/ResizableLayout/hooks/useLayoutItemSizes.js
|
||||
@@ -169,6 +171,7 @@ ReactNativeClient/lib/components/screens/UpgradeSyncTargetScreen.js
|
||||
ReactNativeClient/lib/hooks/useEffectDebugger.js
|
||||
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
|
||||
ReactNativeClient/lib/hooks/usePrevious.js
|
||||
ReactNativeClient/lib/hooks/usePropsDebugger.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
||||
@@ -176,6 +179,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/lib/ntpDate.js
|
||||
ReactNativeClient/lib/services/CommandService.js
|
||||
ReactNativeClient/lib/services/debug/populateDatabase.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
|
||||
|
2
CliClient/package-lock.json
generated
2
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -28,7 +28,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
|
@@ -1,163 +1,163 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint prefer-const: 0*/
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
// require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('test-utils.js');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const Note = require('lib/models/Note');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const ItemChange = require('lib/models/ItemChange');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const ResourceService = require('lib/services/ResourceService.js');
|
||||
// const { time } = require('lib/time-utils.js');
|
||||
// const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('test-utils.js');
|
||||
// const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
// const Note = require('lib/models/Note');
|
||||
// const Folder = require('lib/models/Folder');
|
||||
// const Tag = require('lib/models/Tag');
|
||||
// const ItemChange = require('lib/models/ItemChange');
|
||||
// const Setting = require('lib/models/Setting');
|
||||
// const Resource = require('lib/models/Resource.js');
|
||||
// const { shim } = require('lib/shim');
|
||||
// const ResourceService = require('lib/services/ResourceService.js');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
// process.on('unhandledRejection', (reason, p) => {
|
||||
// console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
// });
|
||||
|
||||
let engine = null;
|
||||
// let engine = null;
|
||||
|
||||
const ids = (array) => array.map(a => a.id);
|
||||
// const ids = (array) => array.map(a => a.id);
|
||||
|
||||
describe('services_SearchFuzzy', function() {
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
// describe('services_SearchFuzzy', function() {
|
||||
// beforeEach(async (done) => {
|
||||
// await setupDatabaseAndSynchronizer(1);
|
||||
// await switchClient(1);
|
||||
|
||||
engine = new SearchEngine();
|
||||
engine.setDb(db());
|
||||
// engine = new SearchEngine();
|
||||
// engine.setDb(db());
|
||||
|
||||
Setting.setValue('db.fuzzySearchEnabled', 1);
|
||||
done();
|
||||
});
|
||||
// Setting.setValue('db.fuzzySearchEnabled', 1);
|
||||
// done();
|
||||
// });
|
||||
|
||||
|
||||
it('should return note almost matching title', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' });
|
||||
const n2 = await Note.save({ title: 'Important note' });
|
||||
// it('should return note almost matching title', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' });
|
||||
// const n2 = await Note.save({ title: 'Important note' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('Broke', { fuzzy: false });
|
||||
expect(rows.length).toBe(0);
|
||||
// await engine.syncTables();
|
||||
// rows = await engine.search('Broke', { fuzzy: false });
|
||||
// expect(rows.length).toBe(0);
|
||||
|
||||
rows = await engine.search('Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// rows = await engine.search('Broke', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('title:Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// rows = await engine.search('title:Broke', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('title:"Broke"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// rows = await engine.search('title:"Broke"', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('Imprtant', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n2.id);
|
||||
}));
|
||||
// rows = await engine.search('Imprtant', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n2.id);
|
||||
// }));
|
||||
|
||||
|
||||
it('should order results by min fuzziness', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'I demand you take me to him' });
|
||||
const n2 = await Note.save({ title: 'He demanded an answer' });
|
||||
const n3 = await Note.save({ title: 'Don\'t you make demands of me' });
|
||||
const n4 = await Note.save({ title: 'No drama for me' });
|
||||
const n5 = await Note.save({ title: 'Just minding my own business' });
|
||||
// it('should order results by min fuzziness', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'I demand you take me to him' });
|
||||
// const n2 = await Note.save({ title: 'He demanded an answer' });
|
||||
// const n3 = await Note.save({ title: 'Don\'t you make demands of me' });
|
||||
// const n4 = await Note.save({ title: 'No drama for me' });
|
||||
// const n5 = await Note.save({ title: 'Just minding my own business' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('demand', { fuzzy: false });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
// await engine.syncTables();
|
||||
// rows = await engine.search('demand', { fuzzy: false });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('demand', { fuzzy: true });
|
||||
expect(rows.length).toBe(3);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
expect(rows[1].id).toBe(n3.id);
|
||||
expect(rows[2].id).toBe(n2.id);
|
||||
}));
|
||||
// rows = await engine.search('demand', { fuzzy: true });
|
||||
// expect(rows.length).toBe(3);
|
||||
// expect(rows[0].id).toBe(n1.id);
|
||||
// expect(rows[1].id).toBe(n3.id);
|
||||
// expect(rows[2].id).toBe(n2.id);
|
||||
// }));
|
||||
|
||||
it('should consider any:1', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'cat' });
|
||||
const n2 = await Note.save({ title: 'cats' });
|
||||
const n3 = await Note.save({ title: 'cot' });
|
||||
// it('should consider any:1', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'cat' });
|
||||
// const n2 = await Note.save({ title: 'cats' });
|
||||
// const n3 = await Note.save({ title: 'cot' });
|
||||
|
||||
const n4 = await Note.save({ title: 'defenestrate' });
|
||||
const n5 = await Note.save({ title: 'defenstrate' });
|
||||
const n6 = await Note.save({ title: 'defenestrated' });
|
||||
// const n4 = await Note.save({ title: 'defenestrate' });
|
||||
// const n5 = await Note.save({ title: 'defenstrate' });
|
||||
// const n6 = await Note.save({ title: 'defenestrated' });
|
||||
|
||||
const n7 = await Note.save({ title: 'he defenestrated the cat' });
|
||||
// const n7 = await Note.save({ title: 'he defenestrated the cat' });
|
||||
|
||||
await engine.syncTables();
|
||||
// await engine.syncTables();
|
||||
|
||||
rows = await engine.search('defenestrated cat', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
// rows = await engine.search('defenestrated cat', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
|
||||
rows = await engine.search('any:1 defenestrated cat', { fuzzy: true });
|
||||
expect(rows.length).toBe(7);
|
||||
}));
|
||||
// rows = await engine.search('any:1 defenestrated cat', { fuzzy: true });
|
||||
// expect(rows.length).toBe(7);
|
||||
// }));
|
||||
|
||||
it('should leave phrase searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'def ghi' });
|
||||
const n3 = await Note.save({ title: 'ghi jkl' });
|
||||
const n4 = await Note.save({ title: 'def abc' });
|
||||
const n5 = await Note.save({ title: 'mno pqr ghi jkl' });
|
||||
// it('should leave phrase searches alone', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'abc def' });
|
||||
// const n2 = await Note.save({ title: 'def ghi' });
|
||||
// const n3 = await Note.save({ title: 'ghi jkl' });
|
||||
// const n4 = await Note.save({ title: 'def abc' });
|
||||
// const n5 = await Note.save({ title: 'mno pqr ghi jkl' });
|
||||
|
||||
await engine.syncTables();
|
||||
// await engine.syncTables();
|
||||
|
||||
rows = await engine.search('abc def', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
// rows = await engine.search('abc def', { fuzzy: true });
|
||||
// expect(rows.length).toBe(2);
|
||||
// expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
|
||||
rows = await engine.search('"abc def"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
// rows = await engine.search('"abc def"', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl"', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
// rows = await engine.search('"ghi jkl"', { fuzzy: true });
|
||||
// expect(rows.length).toBe(2);
|
||||
// expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
// rows = await engine.search('"ghi jkl" mno', { fuzzy: true });
|
||||
// expect(rows.length).toBe(1);
|
||||
// expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
}));
|
||||
// rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true });
|
||||
// expect(rows.length).toBe(2);
|
||||
// expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
// }));
|
||||
|
||||
it('should leave wild card searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'abcc ghi' });
|
||||
const n3 = await Note.save({ title: 'abccc ghi' });
|
||||
const n4 = await Note.save({ title: 'abcccc ghi' });
|
||||
const n5 = await Note.save({ title: 'wxy zzz' });
|
||||
// it('should leave wild card searches alone', asyncTest(async () => {
|
||||
// let rows;
|
||||
// const n1 = await Note.save({ title: 'abc def' });
|
||||
// const n2 = await Note.save({ title: 'abcc ghi' });
|
||||
// const n3 = await Note.save({ title: 'abccc ghi' });
|
||||
// const n4 = await Note.save({ title: 'abcccc ghi' });
|
||||
// const n5 = await Note.save({ title: 'wxy zzz' });
|
||||
|
||||
await engine.syncTables();
|
||||
// await engine.syncTables();
|
||||
|
||||
rows = await engine.search('abc*', { fuzzy: true });
|
||||
// rows = await engine.search('abc*', { fuzzy: true });
|
||||
|
||||
expect(rows.length).toBe(4);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n2.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
}));
|
||||
// expect(rows.length).toBe(4);
|
||||
// expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n2.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
// expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
// }));
|
||||
|
||||
});
|
||||
// });
|
||||
|
@@ -34,6 +34,7 @@ const KeymapService = require('lib/services/KeymapService').default;
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const CssUtils = require('lib/CssUtils');
|
||||
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
|
||||
// const populateDatabase = require('lib/services/debug/populateDatabase').default;
|
||||
const versionInfo = require('lib/versionInfo').default;
|
||||
|
||||
const commands = [
|
||||
@@ -100,6 +101,7 @@ const appDefaultState = Object.assign({}, defaultState, {
|
||||
lastEditorScrollPercents: {},
|
||||
devToolsVisible: false,
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
});
|
||||
|
||||
class Application extends BaseApplication {
|
||||
@@ -291,6 +293,21 @@ class Application extends BaseApplication {
|
||||
delete newState.visibleDialogs[state.name];
|
||||
break;
|
||||
|
||||
case 'FOCUS_SET':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.focusedField = action.field;
|
||||
break;
|
||||
|
||||
case 'FOCUS_CLEAR':
|
||||
|
||||
// A field can only clear its own state
|
||||
if (action.field === state.focusedField) {
|
||||
newState = Object.assign({}, state);
|
||||
newState.focusedField = null;
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||
@@ -1109,7 +1126,7 @@ class Application extends BaseApplication {
|
||||
try {
|
||||
await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`);
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(err.message);
|
||||
reg.logger().error(err.message);
|
||||
}
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
@@ -1267,6 +1284,8 @@ class Application extends BaseApplication {
|
||||
};
|
||||
|
||||
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
|
||||
|
||||
// await populateDatabase(reg.db());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -566,7 +566,7 @@ class MainScreenComponent extends React.Component<any, any> {
|
||||
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
|
||||
} else if (key === 'noteListControls') {
|
||||
return <NoteListControls key={key} />;
|
||||
return <NoteListControls key={key} showNewNoteButtons={this.props.focusedField !== 'globalSearch'} />;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid layout component: ${key}`);
|
||||
@@ -650,6 +650,7 @@ const mapStateToProps = (state:any) => {
|
||||
customCss: state.customCss,
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
|
||||
focusedField: state.focusedField,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -9,6 +9,7 @@ import { useScrollHandler, usePrevious, cursorPositionToTextOffset, useRootSize
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
import NoteTextViewer from '../../../NoteTextViewer';
|
||||
import Editor from './Editor';
|
||||
|
||||
// @ts-ignore
|
||||
@@ -17,7 +18,6 @@ const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const NoteTextViewer = require('../../../NoteTextViewer.min');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -38,6 +38,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
const [webviewReady, setWebviewReady] = useState(false);
|
||||
|
||||
const previousContent = usePrevious(props.content);
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
||||
const previousContentKey = usePrevious(props.contentKey);
|
||||
|
||||
@@ -506,7 +507,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
// If there is a currently active search, it's important to re-search the text as the user
|
||||
// types. However this is slow for performance so we ONLY want it to happen when there is
|
||||
// a search
|
||||
const textChanged = props.searchMarkers.keywords.length > 0 && props.content !== previousContent;
|
||||
|
||||
// Note that since the CodeMirror component also needs to handle the viewer pane, we need
|
||||
// to check if the rendered body has changed too (it will be changed with a delay after
|
||||
// props.content has been updated).
|
||||
const textChanged = props.searchMarkers.keywords.length > 0 && (props.content !== previousContent || renderedBody !== previousRenderedBody);
|
||||
|
||||
if (props.searchMarkers !== previousSearchMarkers || textChanged) {
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
@@ -517,7 +522,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}
|
||||
}
|
||||
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent]);
|
||||
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]);
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
const output = { ...styles.cellEditor };
|
||||
|
@@ -17,13 +17,13 @@ import useCursorUtils from './utils/useCursorUtils';
|
||||
import useLineSorting from './utils/useLineSorting';
|
||||
import useEditorSearch from './utils/useEditorSearch';
|
||||
import useJoplinMode from './utils/useJoplinMode';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
|
||||
import 'codemirror/keymap/emacs';
|
||||
import 'codemirror/keymap/vim';
|
||||
import 'codemirror/keymap/sublime'; // Used for swapLineUp and swapLineDown
|
||||
|
||||
import 'codemirror/mode/meta';
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
@@ -50,7 +50,7 @@ const topLanguages = [
|
||||
'haskell',
|
||||
'pascal',
|
||||
'css',
|
||||
|
||||
|
||||
// Additional languages, not in the PYPL list
|
||||
'xml', // For HTML too
|
||||
'markdown',
|
||||
@@ -99,6 +99,7 @@ function Editor(props: EditorProps, ref: any) {
|
||||
useLineSorting(CodeMirror);
|
||||
useEditorSearch(CodeMirror);
|
||||
useJoplinMode(CodeMirror);
|
||||
useKeymap(CodeMirror);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return editor;
|
||||
@@ -141,87 +142,6 @@ function Editor(props: EditorProps, ref: any) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
CodeMirror.keyMap.basic = {
|
||||
'Left': 'goCharLeft',
|
||||
'Right': 'goCharRight',
|
||||
'Up': 'goLineUp',
|
||||
'Down': 'goLineDown',
|
||||
'End': 'goLineRight',
|
||||
'Home': 'goLineLeftSmart',
|
||||
'PageUp': 'goPageUp',
|
||||
'PageDown': 'goPageDown',
|
||||
'Delete': 'delCharAfter',
|
||||
'Backspace': 'delCharBefore',
|
||||
'Shift-Backspace': 'delCharBefore',
|
||||
'Tab': 'smartListIndent',
|
||||
'Shift-Tab': 'smartListUnindent',
|
||||
'Enter': 'insertListElement',
|
||||
'Insert': 'toggleOverwrite',
|
||||
'Esc': 'singleSelection',
|
||||
};
|
||||
// Add some of the Joplin smart list handling to emacs mode
|
||||
CodeMirror.keyMap.emacs['Tab'] = 'smartListIndent';
|
||||
CodeMirror.keyMap.emacs['Enter'] = 'insertListElement';
|
||||
CodeMirror.keyMap.emacs['Shift-Tab'] = 'smartListUnindent';
|
||||
|
||||
if (shim.isMac()) {
|
||||
CodeMirror.keyMap.default = {
|
||||
// MacOS
|
||||
'Cmd-A': 'selectAll',
|
||||
'Cmd-D': 'deleteLine',
|
||||
'Cmd-Z': 'undo',
|
||||
'Shift-Cmd-Z': 'redo',
|
||||
'Cmd-Y': 'redo',
|
||||
'Cmd-Home': 'goDocStart',
|
||||
'Cmd-Up': 'goDocStart',
|
||||
'Cmd-End': 'goDocEnd',
|
||||
'Cmd-Down': 'goDocEnd',
|
||||
'Cmd-Left': 'goLineLeft',
|
||||
'Cmd-Right': 'goLineRight',
|
||||
'Alt-Left': 'goGroupLeft',
|
||||
'Alt-Right': 'goGroupRight',
|
||||
'Alt-Backspace': 'delGroupBefore',
|
||||
'Alt-Delete': 'delGroupAfter',
|
||||
'Cmd-[': 'indentLess',
|
||||
'Cmd-]': 'indentMore',
|
||||
'Cmd-/': 'toggleComment',
|
||||
'Cmd-Opt-S': 'sortSelectedLines',
|
||||
'Opt-Up': 'swapLineUp',
|
||||
'Opt-Down': 'swapLineDown',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
||||
} else {
|
||||
CodeMirror.keyMap.default = {
|
||||
// Windows/linux
|
||||
'Ctrl-A': 'selectAll',
|
||||
'Ctrl-D': 'deleteLine',
|
||||
'Ctrl-Z': 'undo',
|
||||
'Shift-Ctrl-Z': 'redo',
|
||||
'Ctrl-Y': 'redo',
|
||||
'Ctrl-Home': 'goDocStart',
|
||||
'Ctrl-End': 'goDocEnd',
|
||||
'Ctrl-Up': 'goLineUp',
|
||||
'Ctrl-Down': 'goLineDown',
|
||||
'Ctrl-Left': 'goGroupLeft',
|
||||
'Ctrl-Right': 'goGroupRight',
|
||||
'Alt-Left': 'goLineStart',
|
||||
'Alt-Right': 'goLineEnd',
|
||||
'Ctrl-Backspace': 'delGroupBefore',
|
||||
'Ctrl-Delete': 'delGroupAfter',
|
||||
'Ctrl-[': 'indentLess',
|
||||
'Ctrl-]': 'indentMore',
|
||||
'Ctrl-/': 'toggleComment',
|
||||
'Ctrl-Alt-S': 'sortSelectedLines',
|
||||
'Alt-Up': 'swapLineUp',
|
||||
'Alt-Down': 'swapLineDown',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorParent.current) return () => {};
|
||||
|
||||
|
@@ -0,0 +1,107 @@
|
||||
import { useEffect } from 'react';
|
||||
import CommandService from 'lib/services/CommandService';
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
export default function useKeymap(CodeMirror: any) {
|
||||
|
||||
function save() {
|
||||
CommandService.instance().execute('synchronize');
|
||||
}
|
||||
|
||||
function setupEmacs() {
|
||||
CodeMirror.keyMap.emacs['Tab'] = 'smartListIndent';
|
||||
CodeMirror.keyMap.emacs['Enter'] = 'insertListElement';
|
||||
CodeMirror.keyMap.emacs['Shift-Tab'] = 'smartListUnindent';
|
||||
}
|
||||
|
||||
function setupVim() {
|
||||
CodeMirror.Vim.defineAction('swapLineDown', CodeMirror.commands.swapLineDown);
|
||||
CodeMirror.Vim.mapCommand('<A-j>', 'action', 'swapLineDown', {}, { context: 'normal', isEdit: true });
|
||||
CodeMirror.Vim.defineAction('swapLineUp', CodeMirror.commands.swapLineUp);
|
||||
CodeMirror.Vim.mapCommand('<A-k>', 'action', 'swapLineUp', {}, { context: 'normal', isEdit: true });
|
||||
CodeMirror.Vim.defineAction('insertListElement', CodeMirror.commands.vimInsertListElement);
|
||||
CodeMirror.Vim.mapCommand('o', 'action', 'insertListElement', { after: true }, { context: 'normal', isEdit: true, interlaceInsertRepeat: true });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// This enables the special modes (emacs and vim) to initiate sync by the save action
|
||||
CodeMirror.commands.save = save;
|
||||
|
||||
CodeMirror.keyMap.basic = {
|
||||
'Left': 'goCharLeft',
|
||||
'Right': 'goCharRight',
|
||||
'Up': 'goLineUp',
|
||||
'Down': 'goLineDown',
|
||||
'End': 'goLineRight',
|
||||
'Home': 'goLineLeftSmart',
|
||||
'PageUp': 'goPageUp',
|
||||
'PageDown': 'goPageDown',
|
||||
'Delete': 'delCharAfter',
|
||||
'Backspace': 'delCharBefore',
|
||||
'Shift-Backspace': 'delCharBefore',
|
||||
'Tab': 'smartListIndent',
|
||||
'Shift-Tab': 'smartListUnindent',
|
||||
'Enter': 'insertListElement',
|
||||
'Insert': 'toggleOverwrite',
|
||||
'Esc': 'singleSelection',
|
||||
};
|
||||
|
||||
if (shim.isMac()) {
|
||||
CodeMirror.keyMap.default = {
|
||||
// MacOS
|
||||
'Cmd-A': 'selectAll',
|
||||
'Cmd-D': 'deleteLine',
|
||||
'Cmd-Z': 'undo',
|
||||
'Shift-Cmd-Z': 'redo',
|
||||
'Cmd-Y': 'redo',
|
||||
'Cmd-Home': 'goDocStart',
|
||||
'Cmd-Up': 'goDocStart',
|
||||
'Cmd-End': 'goDocEnd',
|
||||
'Cmd-Down': 'goDocEnd',
|
||||
'Cmd-Left': 'goLineLeft',
|
||||
'Cmd-Right': 'goLineRight',
|
||||
'Alt-Left': 'goGroupLeft',
|
||||
'Alt-Right': 'goGroupRight',
|
||||
'Alt-Backspace': 'delGroupBefore',
|
||||
'Alt-Delete': 'delGroupAfter',
|
||||
'Cmd-[': 'indentLess',
|
||||
'Cmd-]': 'indentMore',
|
||||
'Cmd-/': 'toggleComment',
|
||||
'Cmd-Opt-S': 'sortSelectedLines',
|
||||
'Opt-Up': 'swapLineUp',
|
||||
'Opt-Down': 'swapLineDown',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
||||
} else {
|
||||
CodeMirror.keyMap.default = {
|
||||
// Windows/linux
|
||||
'Ctrl-A': 'selectAll',
|
||||
'Ctrl-D': 'deleteLine',
|
||||
'Ctrl-Z': 'undo',
|
||||
'Shift-Ctrl-Z': 'redo',
|
||||
'Ctrl-Y': 'redo',
|
||||
'Ctrl-Home': 'goDocStart',
|
||||
'Ctrl-End': 'goDocEnd',
|
||||
'Ctrl-Up': 'goLineUp',
|
||||
'Ctrl-Down': 'goLineDown',
|
||||
'Ctrl-Left': 'goGroupLeft',
|
||||
'Ctrl-Right': 'goGroupRight',
|
||||
'Alt-Left': 'goLineStart',
|
||||
'Alt-Right': 'goLineEnd',
|
||||
'Ctrl-Backspace': 'delGroupBefore',
|
||||
'Ctrl-Delete': 'delGroupAfter',
|
||||
'Ctrl-[': 'indentLess',
|
||||
'Ctrl-]': 'indentMore',
|
||||
'Ctrl-/': 'toggleComment',
|
||||
'Ctrl-Alt-S': 'sortSelectedLines',
|
||||
'Alt-Up': 'swapLineUp',
|
||||
'Alt-Down': 'swapLineDown',
|
||||
|
||||
'fallthrough': 'basic',
|
||||
};
|
||||
}
|
||||
setupEmacs();
|
||||
setupVim();
|
||||
}, []);
|
||||
}
|
@@ -126,6 +126,24 @@ export default function useListIdent(CodeMirror: any) {
|
||||
});
|
||||
};
|
||||
|
||||
// This is a special case of insertList element because it happens when
|
||||
// vim is in normal mode and input is disabled and the cursor is not
|
||||
// necessarily at the end of line (but it should pretend it is
|
||||
CodeMirror.commands.vimInsertListElement = function(cm: any) {
|
||||
cm.setOption('disableInput', false);
|
||||
|
||||
const ranges = cm.listSelections();
|
||||
if (ranges.length === 0) return;
|
||||
const { anchor } = ranges[0];
|
||||
|
||||
// Need to move the cursor to end of line as this is the vim behavior
|
||||
const line = cm.getLine(anchor.line);
|
||||
cm.setCursor({ line: anchor.line, ch: line.length });
|
||||
|
||||
cm.execCommand('insertListElement');
|
||||
cm.setOption('disableInput', true);
|
||||
};
|
||||
|
||||
CodeMirror.commands.insertListElement = function(cm: any) {
|
||||
if (cm.getOption('disableInput')) return CodeMirror.Pass;
|
||||
|
||||
|
@@ -338,12 +338,8 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
}
|
||||
|
||||
function renderNoteToolbar() {
|
||||
// const theme = themeStyle(props.themeId);
|
||||
|
||||
const toolbarStyle = {
|
||||
marginBottom: 0,
|
||||
// paddingTop: theme.mainPadding,
|
||||
// paddingBottom: theme.mainPadding,
|
||||
};
|
||||
|
||||
return <NoteToolbar
|
||||
@@ -363,16 +359,12 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
function renderTagBar() {
|
||||
const theme = themeStyle(props.themeId);
|
||||
let control = null;
|
||||
if (!props.selectedNoteTags.length) {
|
||||
const noteIds = [formNote.id];
|
||||
control = <span onClick={() => { CommandService.instance().execute('setTags', { noteIds }); }} style={theme.clickableTextStyle}>Click to add some tags...</span>;
|
||||
} else {
|
||||
control = <TagList items={props.selectedNoteTags} />;
|
||||
}
|
||||
const noteIds = [formNote.id];
|
||||
const instructions = <span onClick={() => { CommandService.instance().execute('setTags', { noteIds }); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>Click to add tags...</span>;
|
||||
const tagList = props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null;
|
||||
|
||||
return (
|
||||
<div style={{ paddingLeft: 8 }}>{control}</div>
|
||||
<div style={{ paddingLeft: 8, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>{tagList}{instructions}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -6,9 +6,12 @@ import CommandService from 'lib/services/CommandService';
|
||||
import { runtime as focusSearchRuntime } from './commands/focusSearch';
|
||||
const styled = require('styled-components').default;
|
||||
|
||||
interface Props {
|
||||
showNewNoteButtons: boolean,
|
||||
}
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
width: 100%;
|
||||
/*height: 100%;*/
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: ${(props:any) => props.theme.mainPadding}px;
|
||||
@@ -19,7 +22,12 @@ const StyledButton = styled(Button)`
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
export default function NoteListControls() {
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
export default function NoteListControls(props:Props) {
|
||||
const searchBarRef = useRef(null);
|
||||
|
||||
useEffect(function() {
|
||||
@@ -38,21 +46,31 @@ export default function NoteListControls() {
|
||||
CommandService.instance().execute('newNote');
|
||||
}
|
||||
|
||||
function renderNewNoteButtons() {
|
||||
if (!props.showNewNoteButtons) return null;
|
||||
|
||||
return (
|
||||
<ButtonContainer>
|
||||
<StyledButton
|
||||
tooltip={CommandService.instance().title('newTodo')}
|
||||
iconName="far fa-check-square"
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={onNewTodoButtonClick}
|
||||
/>
|
||||
<StyledButton
|
||||
tooltip={CommandService.instance().title('newNote')}
|
||||
iconName="icon-note"
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={onNewNoteButtonClick}
|
||||
/>
|
||||
</ButtonContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<SearchBar inputRef={searchBarRef}/>
|
||||
<StyledButton
|
||||
tooltip={CommandService.instance().title('newTodo')}
|
||||
iconName="far fa-check-square"
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={onNewTodoButtonClick}
|
||||
/>
|
||||
<StyledButton
|
||||
tooltip={CommandService.instance().title('newNote')}
|
||||
iconName="icon-note"
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={onNewNoteButtonClick}
|
||||
/>
|
||||
{renderNewNoteButtons()}
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const NoteTextViewer = require('./NoteTextViewer.min');
|
||||
const NoteTextViewer = require('./NoteTextViewer').default;
|
||||
const HelpButton = require('./HelpButton.min');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Revision = require('lib/models/Revision');
|
||||
|
@@ -1,15 +1,24 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
class NoteTextViewerComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
interface Props {
|
||||
onDomReady: Function,
|
||||
onIpcMessage: Function,
|
||||
viewerStyle: any,
|
||||
}
|
||||
|
||||
this.initialized_ = false;
|
||||
this.domReady_ = false;
|
||||
class NoteTextViewerComponent extends React.Component<Props, any> {
|
||||
|
||||
private initialized_:boolean = false;
|
||||
private domReady_:boolean = false;
|
||||
private webviewRef_:any;
|
||||
private webviewListeners_:any = null;
|
||||
|
||||
constructor(props:any) {
|
||||
super(props);
|
||||
|
||||
this.webviewRef_ = React.createRef();
|
||||
this.webviewListeners_ = null;
|
||||
|
||||
this.webview_domReady = this.webview_domReady.bind(this);
|
||||
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
|
||||
@@ -17,20 +26,20 @@ class NoteTextViewerComponent extends React.Component {
|
||||
this.webview_message = this.webview_message.bind(this);
|
||||
}
|
||||
|
||||
webview_domReady(event) {
|
||||
webview_domReady(event:any) {
|
||||
this.domReady_ = true;
|
||||
if (this.props.onDomReady) this.props.onDomReady(event);
|
||||
}
|
||||
|
||||
webview_ipcMessage(event) {
|
||||
webview_ipcMessage(event:any) {
|
||||
if (this.props.onIpcMessage) this.props.onIpcMessage(event);
|
||||
}
|
||||
|
||||
webview_load() {
|
||||
this.webview_domReady();
|
||||
this.webview_domReady({});
|
||||
}
|
||||
|
||||
webview_message(event) {
|
||||
webview_message(event:any) {
|
||||
if (!event.data || event.data.target !== 'main') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
@@ -78,7 +87,14 @@ class NoteTextViewerComponent extends React.Component {
|
||||
wv.removeEventListener(n, fn);
|
||||
}
|
||||
|
||||
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message);
|
||||
try {
|
||||
// It seems this can throw a cross-origin error in a way that is hard to replicate so just wrap
|
||||
// it in try/catch since it's not critical.
|
||||
// https://github.com/laurent22/joplin/issues/3835
|
||||
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message);
|
||||
} catch (error) {
|
||||
reg.logger().warn('Error destroying note viewer', error);
|
||||
}
|
||||
|
||||
this.initialized_ = false;
|
||||
this.domReady_ = false;
|
||||
@@ -107,7 +123,7 @@ class NoteTextViewerComponent extends React.Component {
|
||||
// Wrap WebView functions
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
send(channel, arg0 = null, arg1 = null) {
|
||||
send(channel:string, arg0:any = null, arg1:any = null) {
|
||||
const win = this.webviewRef_.current.contentWindow;
|
||||
|
||||
if (channel === 'setHtml') {
|
||||
@@ -127,36 +143,6 @@ class NoteTextViewerComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
printToPDF() { // options, callback) {
|
||||
// In Electron 4x, printToPDF is broken so need to use this hack:
|
||||
// https://github.com/electron/electron/issues/16171#issuecomment-451090245
|
||||
|
||||
// return this.webviewRef_.current.printToPDF(options, callback);
|
||||
// return this.webviewRef_.current.getWebContents().printToPDF(options, callback);
|
||||
}
|
||||
|
||||
print() {
|
||||
// In Electron 4x, print is broken so need to use this hack:
|
||||
// https://github.com/electron/electron/issues/16219#issuecomment-451454948
|
||||
// Note that this is not a perfect workaround since it means the options are ignored
|
||||
// In particular it means that background images and colours won't be printed (printBackground property will be ignored)
|
||||
|
||||
// return this.webviewRef_.current.getWebContents().print({});
|
||||
return this.webviewRef_.current.getWebContents().executeJavaScript('window.print()');
|
||||
}
|
||||
|
||||
openDevTools() {
|
||||
// return this.webviewRef_.current.openDevTools();
|
||||
}
|
||||
|
||||
closeDevTools() {
|
||||
// return this.webviewRef_.current.closeDevTools();
|
||||
}
|
||||
|
||||
isDevToolsOpened() {
|
||||
// return this.webviewRef_.current.isDevToolsOpened();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Wrap WebView functions (END)
|
||||
// ----------------------------------------------------------------
|
||||
@@ -167,7 +153,7 @@ class NoteTextViewerComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state:any) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
@@ -180,4 +166,4 @@ const NoteTextViewer = connect(
|
||||
{ withRef: true }
|
||||
)(NoteTextViewerComponent);
|
||||
|
||||
module.exports = NoteTextViewer;
|
||||
export default NoteTextViewer;
|
@@ -10,15 +10,42 @@ const { _ } = require('lib/locale.js');
|
||||
interface Props {
|
||||
inputRef?: any,
|
||||
notesParentType: string,
|
||||
dispatch?: Function,
|
||||
}
|
||||
|
||||
function SearchBar(props:Props) {
|
||||
const [query, setQuery] = useState('');
|
||||
const iconName = !query ? CommandService.instance().iconName('search') : 'fa fa-times';
|
||||
|
||||
const onChange = (event:any) => {
|
||||
function onChange(event:any) {
|
||||
setQuery(event.currentTarget.value);
|
||||
};
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
props.dispatch({
|
||||
type: 'FOCUS_SET',
|
||||
field: 'globalSearch',
|
||||
});
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
// Do it after a delay so that the "Clear" button
|
||||
// can be clicked on (otherwise the field loses focus
|
||||
// and is resized before the click event has been processed)
|
||||
setTimeout(() => {
|
||||
props.dispatch({
|
||||
type: 'FOCUS_CLEAR',
|
||||
field: 'globalSearch',
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function onKeyDown(event:any) {
|
||||
if (event.key === 'Escape') {
|
||||
setQuery('');
|
||||
if (document.activeElement) (document.activeElement as any).blur();
|
||||
}
|
||||
}
|
||||
|
||||
const onSearchButtonClick = useCallback(() => {
|
||||
setQuery('');
|
||||
@@ -34,7 +61,16 @@ function SearchBar(props:Props) {
|
||||
|
||||
return (
|
||||
<Root>
|
||||
<SearchInput ref={props.inputRef} value={query} type="text" placeholder={_('Search...')} onChange={onChange}/>
|
||||
<SearchInput
|
||||
ref={props.inputRef}
|
||||
value={query}
|
||||
type="text"
|
||||
placeholder={_('Search...')}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<SearchButton onClick={onSearchButtonClick}>
|
||||
<SearchButtonIcon className={iconName}/>
|
||||
</SearchButton>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { StyledRoot, StyledAddButton, StyledHeader, StyledHeaderIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
|
||||
import { StyledRoot, StyledAddButton, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
|
||||
import { ButtonLevel } from '../Button/Button';
|
||||
import CommandService from 'lib/services/CommandService';
|
||||
|
||||
@@ -45,6 +45,52 @@ const commands = [
|
||||
require('./commands/focusElementSideBar'),
|
||||
];
|
||||
|
||||
function ExpandIcon(props:any) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const style:any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
|
||||
if (!props.isVisible) style.visibility = 'hidden';
|
||||
return <i className={props.isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
|
||||
}
|
||||
|
||||
function ExpandLink(props:any) {
|
||||
return props.hasChildren ? (
|
||||
<StyledExpandLink href="#" data-folder-id={props.folderId} onClick={props.onClick}>
|
||||
<ExpandIcon themeId={props.themeId} isVisible={true} isExpanded={props.isExpanded}/>
|
||||
</StyledExpandLink>
|
||||
) : (
|
||||
<StyledExpandLink><ExpandIcon themeId={props.themeId} isVisible={false} isExpanded={false}/></StyledExpandLink>
|
||||
);
|
||||
}
|
||||
|
||||
function FolderItem(props:any) {
|
||||
const { hasChildren, isExpanded, depth, selected, folderId, folderTitle, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_ } = props;
|
||||
|
||||
const noteCountComp = noteCount ? <StyledNoteCount>{noteCount}</StyledNoteCount> : null;
|
||||
|
||||
return (
|
||||
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth}`} onDragStart={onFolderDragStart_} onDragOver={onFolderDragOver_} onDrop={onFolderDrop_} draggable={true} data-folder-id={folderId}>
|
||||
<ExpandLink themeId={props.themeId} hasChildren={hasChildren} folderId={folderId} onClick={onFolderToggleClick_} isExpanded={isExpanded}/>
|
||||
<StyledListItemAnchor
|
||||
ref={anchorRef}
|
||||
className="list-item"
|
||||
isConflictFolder={folderId === Folder.conflictFolderId()}
|
||||
href="#"
|
||||
selected={selected}
|
||||
data-id={folderId}
|
||||
data-type={BaseModel.TYPE_FOLDER}
|
||||
onContextMenu={itemContextMenu}
|
||||
data-folder-id={folderId}
|
||||
onClick={() => {
|
||||
folderItem_click(folderId);
|
||||
}}
|
||||
onDoubleClick={onFolderToggleClick_}
|
||||
>
|
||||
{folderTitle} {noteCountComp}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
);
|
||||
}
|
||||
|
||||
class SideBarComponent extends React.Component<Props, State> {
|
||||
|
||||
private folderItemsOrder_:any[] = [];
|
||||
@@ -68,6 +114,8 @@ class SideBarComponent extends React.Component<Props, State> {
|
||||
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this);
|
||||
this.header_contextMenu = this.header_contextMenu.bind(this);
|
||||
this.onAddFolderButtonClick = this.onAddFolderButtonClick.bind(this);
|
||||
this.folderItem_click = this.folderItem_click.bind(this);
|
||||
this.itemContextMenu = this.itemContextMenu.bind(this);
|
||||
}
|
||||
|
||||
onFolderDragStart_(event:any) {
|
||||
@@ -257,10 +305,10 @@ class SideBarComponent extends React.Component<Props, State> {
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
|
||||
folderItem_click(folder:any) {
|
||||
folderItem_click(folderId:string) {
|
||||
this.props.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder ? folder.id : null,
|
||||
id: folderId ? folderId : null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,7 +339,7 @@ class SideBarComponent extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
renderNoteCount(count:number) {
|
||||
return <StyledNoteCount>{count}</StyledNoteCount>;
|
||||
return count ? <StyledNoteCount>{count}</StyledNoteCount> : null;
|
||||
}
|
||||
|
||||
renderExpandIcon(isExpanded:boolean, isVisible:boolean = true) {
|
||||
@@ -305,57 +353,41 @@ class SideBarComponent extends React.Component<Props, State> {
|
||||
return (
|
||||
<StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0'} isSpecialItem={true}>
|
||||
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
|
||||
<StyledAllNotesIcon className="icon-notes"/>
|
||||
<StyledListItemAnchor
|
||||
className="list-item"
|
||||
isSpecialItem={true}
|
||||
href="#"
|
||||
selected={selected}
|
||||
onClick={() => {
|
||||
this.onAllNotesClick_();
|
||||
}}
|
||||
onClick={this.onAllNotesClick_}
|
||||
>
|
||||
({_('All notes')})
|
||||
{_('All notes')}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderFolderItem(folder:any, selected:boolean, hasChildren:boolean, depth:number) {
|
||||
const isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0;
|
||||
const expandIcon = this.renderExpandIcon(isExpanded, hasChildren);
|
||||
const expandLink = hasChildren ? (
|
||||
<StyledExpandLink href="#" data-folder-id={folder.id} onClick={this.onFolderToggleClick_}>
|
||||
{expandIcon}
|
||||
</StyledExpandLink>
|
||||
) : (
|
||||
<StyledExpandLink>{expandIcon}</StyledExpandLink>
|
||||
);
|
||||
|
||||
const anchorRef = this.anchorItemRef('folder', folder.id);
|
||||
const noteCount = folder.note_count ? this.renderNoteCount(folder.note_count) : '';
|
||||
|
||||
return (
|
||||
<StyledListItem depth={depth} selected={selected} className={`list-item-container list-item-depth-${depth}`} key={folder.id} onDragStart={this.onFolderDragStart_} onDragOver={this.onFolderDragOver_} onDrop={this.onFolderDrop_} draggable={true} data-folder-id={folder.id}>
|
||||
{expandLink}
|
||||
<StyledListItemAnchor
|
||||
ref={anchorRef}
|
||||
className="list-item"
|
||||
isConflictFolder={folder.id === Folder.conflictFolderId()}
|
||||
href="#"
|
||||
selected={selected}
|
||||
data-id={folder.id}
|
||||
data-type={BaseModel.TYPE_FOLDER}
|
||||
onContextMenu={(event:any) => this.itemContextMenu(event)}
|
||||
data-folder-id={folder.id}
|
||||
onClick={() => {
|
||||
this.folderItem_click(folder);
|
||||
}}
|
||||
onDoubleClick={this.onFolderToggleClick_}
|
||||
>
|
||||
{Folder.displayTitle(folder)} {noteCount}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
);
|
||||
return <FolderItem
|
||||
key={folder.id}
|
||||
folderId={folder.id}
|
||||
folderTitle={Folder.displayTitle(folder)}
|
||||
themeId={this.props.themeId}
|
||||
depth={depth}
|
||||
selected={selected}
|
||||
isExpanded={this.props.collapsedFolderIds.indexOf(folder.id) < 0}
|
||||
hasChildren={hasChildren}
|
||||
anchorRef={anchorRef}
|
||||
noteCount={folder.note_count}
|
||||
onFolderDragStart_={this.onFolderDragStart_}
|
||||
onFolderDragOver_={this.onFolderDragOver_}
|
||||
onFolderDrop_={this.onFolderDrop_}
|
||||
itemContextMenu={this.itemContextMenu}
|
||||
folderItem_click={this.folderItem_click}
|
||||
onFolderToggleClick_={this.onFolderToggleClick_}
|
||||
/>;
|
||||
}
|
||||
|
||||
renderTag(tag:any, selected:boolean) {
|
||||
@@ -372,7 +404,7 @@ class SideBarComponent extends React.Component<Props, State> {
|
||||
selected={selected}
|
||||
data-id={tag.id}
|
||||
data-type={BaseModel.TYPE_TAG}
|
||||
onContextMenu={(event:any) => this.itemContextMenu(event)}
|
||||
onContextMenu={this.itemContextMenu}
|
||||
onClick={() => {
|
||||
this.tagItem_click(tag);
|
||||
}}
|
||||
|
@@ -31,6 +31,12 @@ export const StyledHeaderIcon = styled.i`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
export const StyledAllNotesIcon = styled(StyledHeaderIcon)`
|
||||
font-size: ${(props:any) => props.theme.toolbarIconSize * 0.8}px;
|
||||
color: ${(props:any) => props.theme.colorFaded2};
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
export const StyledHeaderLabel = styled.span`
|
||||
flex: 1;
|
||||
color: ${(props:any) => props.theme.color2};
|
||||
@@ -43,9 +49,10 @@ export const StyledListItem = styled.div`
|
||||
height: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${(props:any) => props.theme.mainPadding + ('depth' in props ? props.depth : 0) * 16}px;
|
||||
background: ${(props:any) => props.selected ? props.theme.selectedColor2 : 'none'};
|
||||
text-transform: ${(props:any) => props.isSpecialItem ? 'uppercase' : 'none'};
|
||||
/*text-transform: ${(props:any) => props.isSpecialItem ? 'uppercase' : 'none'};*/
|
||||
transition: 0.1s;
|
||||
|
||||
&:hover {
|
||||
|
2
ElectronClient/package-lock.json
generated
2
ElectronClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
@@ -125,8 +125,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097578
|
||||
versionName "1.2.1"
|
||||
versionCode 2097579
|
||||
versionName "1.2.2"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -499,7 +499,11 @@ class BaseApplication {
|
||||
refreshNotesUseSelectedNoteId = true;
|
||||
}
|
||||
|
||||
if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') {
|
||||
// Should refresh the notes when:
|
||||
// - A tag is selected, to show the notes for that tag
|
||||
// - When a tag is updated so that when searching by tags, the search results are updated
|
||||
// https://github.com/laurent22/joplin/issues/3754
|
||||
if (['TAG_SELECT', 'TAG_DELETE', 'TAG_UPDATE_ONE', 'NOTE_TAG_REMOVE'].includes(action.type)) {
|
||||
refreshNotes = true;
|
||||
}
|
||||
|
||||
@@ -736,18 +740,23 @@ class BaseApplication {
|
||||
setLocale(Setting.value('locale'));
|
||||
}
|
||||
|
||||
if (Setting.value('db.fuzzySearchEnabled') === -1) {
|
||||
const fuzzySearchEnabled = await this.database_.fuzzySearchEnabled();
|
||||
Setting.setValue('db.fuzzySearchEnabled', fuzzySearchEnabled ? 1 : 0);
|
||||
}
|
||||
// if (Setting.value('db.fuzzySearchEnabled') === -1) {
|
||||
// const fuzzySearchEnabled = await this.database_.fuzzySearchEnabled();
|
||||
// Setting.setValue('db.fuzzySearchEnabled', fuzzySearchEnabled ? 1 : 0);
|
||||
// }
|
||||
|
||||
// Always disable on CLI because building and packaging the extension is not working
|
||||
// and is too error-prone - requires gcc on the machine, or we should package the .so
|
||||
// and dylib files, but it's not sure it would work everywhere if not built from
|
||||
// source on the target machine.
|
||||
if (Setting.value('appType') !== 'desktop') {
|
||||
Setting.setValue('db.fuzzySearchEnabled', 0);
|
||||
}
|
||||
// // Always disable on CLI because building and packaging the extension is not working
|
||||
// // and is too error-prone - requires gcc on the machine, or we should package the .so
|
||||
// // and dylib files, but it's not sure it would work everywhere if not built from
|
||||
// // source on the target machine.
|
||||
// if (Setting.value('appType') !== 'desktop') {
|
||||
// Setting.setValue('db.fuzzySearchEnabled', 0);
|
||||
// }
|
||||
|
||||
// For now always disable fuzzy search due to performance issues:
|
||||
// https://discourse.joplinapp.org/t/1-1-4-keyboard-locks-up-while-typing/11231/11
|
||||
// https://discourse.joplinapp.org/t/serious-lagging-when-there-are-tens-of-thousands-of-notes/11215/23
|
||||
Setting.setValue('db.fuzzySearchEnabled', 0);
|
||||
|
||||
if (Setting.value('encryption.shouldReencrypt') < 0) {
|
||||
// We suggest re-encryption if the user has at least one notebook
|
||||
|
@@ -11,6 +11,7 @@ const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const SearchEngineUtils = require('lib/services/searchengine/SearchEngineUtils');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
|
||||
Icon.loadFont();
|
||||
|
||||
@@ -72,18 +73,6 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
// UNSAFE_componentWillReceiveProps(newProps) {
|
||||
// console.info('UNSAFE_componentWillReceiveProps', newProps);
|
||||
|
||||
// let newState = {};
|
||||
// if ('query' in newProps && !this.state.query) newState.query = newProps.query;
|
||||
|
||||
// if (Object.getOwnPropertyNames(newState).length) {
|
||||
// this.setState(newState);
|
||||
// this.refreshSearch(newState.query);
|
||||
// }
|
||||
// }
|
||||
|
||||
searchTextInput_submit() {
|
||||
const query = this.state.query.trim();
|
||||
if (!query) return;
|
||||
@@ -134,6 +123,14 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
|
||||
if (!this.isMounted_) return;
|
||||
|
||||
const parsedQuery = await SearchEngine.instance().parseQuery(query);
|
||||
const highlightedWords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'SET_HIGHLIGHTED',
|
||||
words: highlightedWords,
|
||||
});
|
||||
|
||||
this.setState({ notes: notes });
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,8 @@ class Database {
|
||||
this.logger_ = new Logger();
|
||||
this.logExcludedQueryTypes_ = [];
|
||||
this.batchTransactionMutex_ = new Mutex();
|
||||
this.profilingEnabled_ = false;
|
||||
this.queryId_ = 1;
|
||||
}
|
||||
|
||||
setLogExcludedQueryTypes(v) {
|
||||
@@ -71,10 +73,30 @@ class Database {
|
||||
|
||||
let waitTime = 50;
|
||||
let totalWaitTime = 0;
|
||||
const callStartTime = Date.now();
|
||||
let profilingTimeoutId = null;
|
||||
while (true) {
|
||||
try {
|
||||
this.logQuery(sql, params);
|
||||
|
||||
const queryId = this.queryId_++;
|
||||
if (this.profilingEnabled_) {
|
||||
console.info(`SQL START ${queryId}`, sql, params);
|
||||
|
||||
profilingTimeoutId = setInterval(() => {
|
||||
console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
const result = await this.driver()[callName](sql, params);
|
||||
|
||||
if (this.profilingEnabled_) {
|
||||
clearInterval(profilingTimeoutId);
|
||||
profilingTimeoutId = null;
|
||||
const elapsed = Date.now() - callStartTime;
|
||||
if (elapsed > 10) console.info(`SQL END ${queryId}`, elapsed, sql, params);
|
||||
}
|
||||
|
||||
return result; // No exception was thrown
|
||||
} catch (error) {
|
||||
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
|
||||
@@ -89,6 +111,8 @@ class Database {
|
||||
} else {
|
||||
throw this.sqliteErrorToJsError(error, sql, params);
|
||||
}
|
||||
} finally {
|
||||
if (profilingTimeoutId) clearInterval(profilingTimeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,14 +121,16 @@ class Database {
|
||||
return this.tryCall('selectOne', sql, params);
|
||||
}
|
||||
|
||||
async loadExtension(path) {
|
||||
let result = null;
|
||||
try {
|
||||
result = await this.driver().loadExtension(path);
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw new Error(`Could not load extension ${path}`);
|
||||
}
|
||||
async loadExtension(/* path */) {
|
||||
return; // Disabled for now as fuzzy search extension is not in use
|
||||
|
||||
// let result = null;
|
||||
// try {
|
||||
// result = await this.driver().loadExtension(path);
|
||||
// return result;
|
||||
// } catch (e) {
|
||||
// throw new Error(`Could not load extension ${path}`);
|
||||
// }
|
||||
}
|
||||
|
||||
async selectAll(sql, params = null) {
|
||||
|
13
ReactNativeClient/lib/hooks/usePropsDebugger.ts
Normal file
13
ReactNativeClient/lib/hooks/usePropsDebugger.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import useEffectDebugger from './useEffectDebugger';
|
||||
|
||||
export default function usePropsDebugger(effectHook:any, props:any) {
|
||||
const dependencies:any[] = [];
|
||||
const dependencyNames:string[] = [];
|
||||
|
||||
for (const k in props) {
|
||||
dependencies.push(props[k]);
|
||||
dependencyNames.push(k);
|
||||
}
|
||||
|
||||
useEffectDebugger(effectHook, dependencies, dependencyNames);
|
||||
}
|
@@ -864,7 +864,7 @@ class JoplinDatabase extends Database {
|
||||
if (targetVersion === 15 || targetVersion === 18 || targetVersion === 33) {
|
||||
this.logger().warn('Could not upgrade to database v15 or v18 or v33 - FTS feature will not be used', error);
|
||||
} else if (targetVersion === 34) {
|
||||
this.logger().warn('Could not upgrade to database v34 - fuzzy search will not be used', error);
|
||||
// this.logger().warn('Could not upgrade to database v34 - fuzzy search will not be used', error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
@@ -286,6 +286,7 @@ module.exports = function(theme) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.jop-tinymce ul.joplin-checklist .checked,
|
||||
.md-checkbox .checkbox-label-checked {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
@@ -156,6 +156,8 @@ class BaseItem extends BaseModel {
|
||||
}
|
||||
|
||||
static async loadItemsByIds(ids) {
|
||||
if (!ids.length) return [];
|
||||
|
||||
const classes = this.syncItemClassNames();
|
||||
let output = [];
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
|
@@ -109,6 +109,7 @@ class Tag extends BaseItem {
|
||||
|
||||
static async tagsByNoteId(noteId) {
|
||||
const tagIds = await NoteTag.tagIdsByNoteId(noteId);
|
||||
if (!tagIds.length) return [];
|
||||
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${tagIds.join('","')}")`);
|
||||
}
|
||||
|
||||
|
56
ReactNativeClient/lib/services/debug/populateDatabase.ts
Normal file
56
ReactNativeClient/lib/services/debug/populateDatabase.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Note = require('lib/models/Note');
|
||||
|
||||
function randomIndex(array:any[]):number {
|
||||
return Math.round(Math.random() * (array.length - 1));
|
||||
}
|
||||
|
||||
export default async function populateDatabase(db:any) {
|
||||
await db.clearForTesting();
|
||||
|
||||
const folderCount = 2000;
|
||||
const noteCount = 20000;
|
||||
|
||||
const createdFolderIds:string[] = [];
|
||||
const createdNoteIds:string[] = [];
|
||||
|
||||
for (let i = 0; i < folderCount; i++) {
|
||||
const folder:any = {
|
||||
title: `folder${i}`,
|
||||
};
|
||||
|
||||
const isRoot = Math.random() <= 0.1 || i === 0;
|
||||
|
||||
if (!isRoot) {
|
||||
const parentIndex = randomIndex(createdFolderIds);
|
||||
folder.parent_id = createdFolderIds[parentIndex];
|
||||
}
|
||||
|
||||
const savedFolder = await Folder.save(folder);
|
||||
createdFolderIds.push(savedFolder.id);
|
||||
|
||||
console.info(`Folders: ${i} / ${folderCount}`);
|
||||
}
|
||||
|
||||
let noteBatch = [];
|
||||
for (let i = 0; i < noteCount; i++) {
|
||||
const note:any = { title: `note${i}`, body: `This is note num. ${i}` };
|
||||
const parentIndex = randomIndex(createdFolderIds);
|
||||
note.parent_id = createdFolderIds[parentIndex];
|
||||
|
||||
noteBatch.push(Note.save(note, { dispatchUpdateAction: false }).then((savedNote:any) => {
|
||||
createdNoteIds.push(savedNote.id);
|
||||
console.info(`Notes: ${i} / ${noteCount}`);
|
||||
}));
|
||||
|
||||
if (noteBatch.length > 1000) {
|
||||
await Promise.all(noteBatch);
|
||||
noteBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (noteBatch.length) {
|
||||
await Promise.all(noteBatch);
|
||||
noteBatch = [];
|
||||
}
|
||||
}
|
@@ -460,6 +460,15 @@ class SearchEngine {
|
||||
const fuzzyTitle = await this.fuzzifier(titleTerms.filter(x => !x.wildcard).map(x => trimQuotes(x.value)));
|
||||
const fuzzyBody = await this.fuzzifier(bodyTerms.filter(x => !x.wildcard).map(x => trimQuotes(x.value)));
|
||||
|
||||
// Floor the fuzzy scores to 0, 1 and 2.
|
||||
const floorFuzzyScore = (matches) => {
|
||||
for (let i = 0; i < matches.length; i++) matches[i].score = i;
|
||||
};
|
||||
|
||||
fuzzyText.forEach(floorFuzzyScore);
|
||||
fuzzyTitle.forEach(floorFuzzyScore);
|
||||
fuzzyBody.forEach(floorFuzzyScore);
|
||||
|
||||
const phraseTextSearch = textTerms.filter(x => x.quoted);
|
||||
const wildCardSearch = textTerms.concat(titleTerms).concat(bodyTerms).filter(x => x.wildcard);
|
||||
|
||||
|
@@ -1,5 +1,11 @@
|
||||
# Joplin terminal app changelog
|
||||
|
||||
## [cli-v1.2.2](https://github.com/laurent22/joplin/releases/tag/cli-v1.2.2) - 2020-09-29T11:33:53Z
|
||||
|
||||
- Fixed: Fixed crash due to missing spellfix extension
|
||||
- Fixed: Fixed link generation when exporting to PDF or HTML (#3780)
|
||||
- Fixed: Improved handling of special characters when exporting to Markdown (#3760)
|
||||
|
||||
## [cli-v1.2.1](https://github.com/laurent22/joplin/releases/tag/cli-v1.2.1) - 2020-09-23T11:15:12Z
|
||||
|
||||
- Fixed: Fixed crash due to missing spellfix extension
|
||||
|
Reference in New Issue
Block a user