diff --git a/CliClient/run_test.sh b/CliClient/run_test.sh index a6703fcf3..89a6801eb 100755 --- a/CliClient/run_test.sh +++ b/CliClient/run_test.sh @@ -22,7 +22,6 @@ trap finish EXIT cd "$ROOT_DIR" npm test tests-build/ArrayUtils.js -npm test tests-build/encryption.js npm test tests-build/EnexToMd.js npm test tests-build/HtmlToMd.js npm test tests-build/markdownUtils.js @@ -32,5 +31,7 @@ npm test tests-build/models_Tag.js npm test tests-build/models_Setting.js npm test tests-build/services_InteropService.js npm test tests-build/services_ResourceService.js -npm test tests-build/synchronizer.js -npm test tests-build/urlUtils.js \ No newline at end of file +npm test tests-build/urlUtils.js +npm test tests-build/encryption.js +npm test tests-build/services_rest_Api.js +npm test tests-build/synchronizer.js \ No newline at end of file diff --git a/CliClient/tests/services_rest_Api.js b/CliClient/tests/services_rest_Api.js index a9c70eff4..e932b7567 100644 --- a/CliClient/tests/services_rest_Api.js +++ b/CliClient/tests/services_rest_Api.js @@ -6,6 +6,7 @@ const markdownUtils = require('lib/markdownUtils.js'); const Api = require('lib/services/rest/Api'); const Folder = require('lib/models/Folder'); const Note = require('lib/models/Note'); +const Tag = require('lib/models/Tag'); const Resource = require('lib/models/Resource'); jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; @@ -45,6 +46,60 @@ describe('services_rest_Api', function() { done(); }); + it('should update folders', async (done) => { + let f1 = await Folder.save({ title: "mon carnet" }); + const response = await api.route('PUT', 'folders/' + f1.id, null, JSON.stringify({ + title: 'modifié', + })); + + let f1b = await Folder.load(f1.id); + expect(f1b.title).toBe('modifié'); + + done(); + }); + + it('should delete folders', async (done) => { + let f1 = await Folder.save({ title: "mon carnet" }); + await api.route('DELETE', 'folders/' + f1.id); + + let f1b = await Folder.load(f1.id); + expect(!f1b).toBe(true); + + done(); + }); + + it('should create folders', async (done) => { + const response = await api.route('POST', 'folders', null, JSON.stringify({ + title: 'from api', + })); + + expect(!!response.id).toBe(true); + + let f = await Folder.all(); + expect(f.length).toBe(1); + expect(f[0].title).toBe('from api'); + + done(); + }); + + it('should get one folder', async (done) => { + let f1 = await Folder.save({ title: "mon carnet" }); + const response = await api.route('GET', 'folders/' + f1.id); + expect(response.id).toBe(f1.id); + + const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'folders/doesntexist')); + expect(hasThrown).toBe(true); + + done(); + }); + + it('should fail on invalid paths', async (done) => { + const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'schtroumpf')); + expect(hasThrown).toBe(true); + + done(); + }); + it('should get notes', async (done) => { let response = null; const f1 = await Folder.save({ title: "mon carnet" }); @@ -159,4 +214,44 @@ describe('services_rest_Api', function() { done(); }); + it('should add tags to notes', async (done) => { + const tag = await Tag.save({ title: "mon étiquette" }); + const note = await Note.save({ title: "ma note" }); + + const response = await api.route('POST', 'tags/' + tag.id + '/notes', null, JSON.stringify({ + id: note.id, + })); + + const noteIds = await Tag.noteIds(tag.id); + expect(noteIds[0]).toBe(note.id); + + done(); + }); + + it('should remove tags from notes', async (done) => { + const tag = await Tag.save({ title: "mon étiquette" }); + const note = await Note.save({ title: "ma note" }); + await Tag.addNote(tag.id, note.id); + + const response = await api.route('DELETE', 'tags/' + tag.id + '/notes/' + note.id); + + const noteIds = await Tag.noteIds(tag.id); + expect(noteIds.length).toBe(0); + + done(); + }); + + it('should list all tag notes', async (done) => { + const tag = await Tag.save({ title: "mon étiquette" }); + const note1 = await Note.save({ title: "ma note un" }); + const note2 = await Note.save({ title: "ma note deux" }); + await Tag.addNote(tag.id, note1.id); + await Tag.addNote(tag.id, note2.id); + + const response = await api.route('GET', 'tags/' + tag.id + '/notes'); + expect(response.length).toBe(2); + + done(); + }); + }); \ No newline at end of file diff --git a/ElectronClient/app/gui/ClipperConfigScreen.jsx b/ElectronClient/app/gui/ClipperConfigScreen.jsx index 471c1db06..1579bd985 100644 --- a/ElectronClient/app/gui/ClipperConfigScreen.jsx +++ b/ElectronClient/app/gui/ClipperConfigScreen.jsx @@ -7,9 +7,16 @@ const { themeStyle } = require('../theme.js'); const { _ } = require('lib/locale.js'); const ClipperServer = require('lib/ClipperServer'); const Setting = require('lib/models/Setting'); +const { clipboard } = require('electron'); class ClipperConfigScreenComponent extends React.Component { + constructor() { + super(); + + this.copyToken_click = this.copyToken_click.bind(this); + } + disableClipperServer_click() { Setting.setValue('clipperServer.autoStart', false); ClipperServer.instance().stop(); @@ -28,6 +35,12 @@ class ClipperConfigScreenComponent extends React.Component { bridge().openExternal("https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"); } + copyToken_click() { + clipboard.writeText(this.props.apiToken); + + alert(_('Token has been copied to the clipboard!')); + } + render() { const style = this.props.style; const theme = themeStyle(this.props.theme); @@ -58,27 +71,43 @@ class ClipperConfigScreenComponent extends React.Component { webClipperStatusComps.push() } + const apiTokenStyle = Object.assign({}, theme.textStyle, { + color: theme.colorFaded, + wordBreak: 'break-all', + paddingTop: 10, + paddingBottom: 10, + }); + return (
{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}
-{_('In order to use the web clipper, you need to do the following:')}
+{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}
+{_('In order to use the web clipper, you need to do the following:')}
-{_('Step 1: Enable the clipper service')}
-{_('This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.')}
-{_('Step 1: Enable the clipper service')}
+{_('This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.')}
+{_('Step 2: Install the extension')}
-{_('Download and install the relevant extension for your browser:')}
-{_('Step 2: Install the extension')}
+{_('Download and install the relevant extension for your browser:')}
+ +{_('Advanced options')}
+{_('Authorisation token:')}
+{this.props.apiToken} {_('Copy token')}
+{_('This authorisation token is only needed to allow third-party applications to access Joplin.')}