1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-05 20:56:22 +02:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Laurent Cozic
88d027625a Server v2.0.1 2021-05-14 16:00:36 +02:00
Laurent Cozic
dde34f1d7d force local 2021-05-14 15:45:32 +02:00
Laurent Cozic
dd6d0482e1 new version 2021-05-14 15:05:11 +02:00
Laurent Cozic
bcaad10d74 release 2021-05-14 15:03:55 +02:00
Laurent Cozic
81884cf2ea add comment 2021-05-14 14:52:29 +02:00
Laurent Cozic
d7ff634f5e Merge branch 'release-1.8' into dev 2021-05-14 14:50:04 +02:00
Laurent Cozic
a965da97b6 ios-v10.8.1 2021-05-14 14:43:49 +02:00
Laurent Cozic
b99cb0248d Cli: Fixes #4845: Fixed possible crash when trying to delete corrupted revision in revision service 2021-05-14 12:09:20 +02:00
Laurent Cozic
a31b402b9e Desktop: Added button to skip an application update
And made auto-updates enabled by default
2021-05-14 11:29:06 +02:00
Laurent Cozic
6959f14a3f Desktop: Fixes #4958: Display proper error message when JEX file is corrupted 2021-05-13 22:13:53 +02:00
Laurent
0765cf5955 All: Add support for sharing notebooks with Joplin Server (#4772)
- Joplin Server: Adds support for sharing a notebook
- Desktop: Adds support for sharing a notebook with Joplin Server
- Mobile: Adds support for reading and writing to a shared notebook (not possible to share a notebook)
- Cli: Adds support for reading and writing to a shared notebook (not possible to share a notebook)
2021-05-13 17:57:37 +01:00
JackGruber
09ad70983a Desktop: Fix #4581: Show or hide completed todos in search results based on user settings (#4951) 2021-05-13 13:23:17 +01:00
Helmut K. C. Tessarek
6e64b872cf Update translations 2021-05-13 05:34:25 -04:00
Helmut K. C. Tessarek
d26b92500c All: Translation: Update da_DK.po (thanks ERYpTION) 2021-05-13 05:29:16 -04:00
Helmut K. C. Tessarek
fc9aa33dbb Update readme downloads 2021-05-13 05:23:54 -04:00
Helmut K. C. Tessarek
a286dbdf86 update website 2021-05-13 05:17:36 -04:00
Helmut K. C. Tessarek
c3f2bce818 fix a few typos and grammar 2021-05-13 05:11:10 -04:00
Subhra264
df6f0ce9af Desktop: Fixes #4891: Solve "Resource Id not provided" error (#4943) 2021-05-13 09:34:03 +01:00
JackGruber
5f2998a6e2 Doc: Add sync status to FAQ (#4953) 2021-05-13 08:56:31 +01:00
Helmut K. C. Tessarek
fa6981faa8 All: Add new date format YYMMDD (#4954) 2021-05-13 08:55:41 +01:00
JackGruber
ce80d7e883 Generator: Fixed issue with handling of CRLF in ignore file (#4956) 2021-05-13 08:54:27 +01:00
Laurent Cozic
1a84ca204e Merge branch 'release-1.8' into dev 2021-05-11 15:05:01 +02:00
Edvin Saletovic
0d523e5394 All: Translation: Update bs_BA.po (#4942) 2021-05-10 16:27:31 -04:00
Laurent Cozic
b28f087bbe Desktop release v1.8.5 2021-05-10 12:33:18 +02:00
Laurent Cozic
ea536dbf87 Merge branch 'release-1.8' into dev 2021-05-10 12:23:39 +02:00
Laurent Cozic
ed19424271 Plugin Generator release v1.8.1 2021-05-10 12:23:14 +02:00
Laurent Cozic
5d7a3ceff5 Doc: Fixed deploy doc 2021-05-10 12:22:56 +02:00
Laurent Cozic
610e1dc885 Doc: Add info on how to deploy each app 2021-05-10 12:21:55 +02:00
Laurent Cozic
10bb689d5f Merge branch 'release-1.8' into dev 2021-05-09 20:07:40 +02:00
Woosuk Park
99410005a6 All: Translation: Update ko.po (#4930) 2021-05-08 15:07:47 -04:00
291 changed files with 19636 additions and 12414 deletions

View File

@@ -18,6 +18,7 @@ packages/turndown-plugin-gfm/
node_modules/
packages/lib/lib/lib.js
packages/lib/locales/index.js
packages/lib/services/database/types.ts
packages/app-cli/build
packages/app-cli/build/
packages/app-cli/locales
@@ -109,6 +110,12 @@ packages/app-cli/tests/Synchronizer.tags.js.map
packages/app-cli/tests/Synchronizer.tools.d.ts
packages/app-cli/tests/Synchronizer.tools.js
packages/app-cli/tests/Synchronizer.tools.js.map
packages/app-cli/tests/dateTimeFormats.d.ts
packages/app-cli/tests/dateTimeFormats.js
packages/app-cli/tests/dateTimeFormats.js.map
packages/app-cli/tests/file-api-driver.d.ts
packages/app-cli/tests/file-api-driver.js
packages/app-cli/tests/file-api-driver.js.map
packages/app-cli/tests/fsDriver.d.ts
packages/app-cli/tests/fsDriver.js
packages/app-cli/tests/fsDriver.js.map
@@ -118,6 +125,9 @@ packages/app-cli/tests/htmlUtils.js.map
packages/app-cli/tests/models_Folder.d.ts
packages/app-cli/tests/models_Folder.js
packages/app-cli/tests/models_Folder.js.map
packages/app-cli/tests/models_Folder.sharing.d.ts
packages/app-cli/tests/models_Folder.sharing.js
packages/app-cli/tests/models_Folder.sharing.js.map
packages/app-cli/tests/models_Note.d.ts
packages/app-cli/tests/models_Note.js
packages/app-cli/tests/models_Note.js.map
@@ -157,6 +167,9 @@ packages/app-cli/tests/services_PluginService.js.map
packages/app-cli/tests/services_ResourceService.d.ts
packages/app-cli/tests/services_ResourceService.js
packages/app-cli/tests/services_ResourceService.js.map
packages/app-cli/tests/services_SearchEngineUtils.d.ts
packages/app-cli/tests/services_SearchEngineUtils.js
packages/app-cli/tests/services_SearchEngineUtils.js.map
packages/app-cli/tests/services_keychainService.d.ts
packages/app-cli/tests/services_keychainService.js
packages/app-cli/tests/services_keychainService.js.map
@@ -187,6 +200,9 @@ packages/app-desktop/app.js.map
packages/app-desktop/bridge.d.ts
packages/app-desktop/bridge.js
packages/app-desktop/bridge.js.map
packages/app-desktop/checkForUpdates.d.ts
packages/app-desktop/checkForUpdates.js
packages/app-desktop/checkForUpdates.js.map
packages/app-desktop/commands/copyDevCommand.d.ts
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyDevCommand.js.map
@@ -244,6 +260,15 @@ packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js.ma
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.d.ts
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js.map
packages/app-desktop/gui/Dialog.d.ts
packages/app-desktop/gui/Dialog.js
packages/app-desktop/gui/Dialog.js.map
packages/app-desktop/gui/DialogButtonRow.d.ts
packages/app-desktop/gui/DialogButtonRow.js
packages/app-desktop/gui/DialogButtonRow.js.map
packages/app-desktop/gui/DialogTitle.d.ts
packages/app-desktop/gui/DialogTitle.js
packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@@ -331,6 +356,9 @@ packages/app-desktop/gui/MainScreen/commands/showNoteContentProperties.js.map
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.d.ts
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js.map
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.d.ts
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js.map
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.d.ts
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js.map
@@ -583,6 +611,9 @@ packages/app-desktop/gui/Root_UpgradeSyncTarget.js.map
packages/app-desktop/gui/SearchBar/SearchBar.d.ts
packages/app-desktop/gui/SearchBar/SearchBar.js
packages/app-desktop/gui/SearchBar/SearchBar.js.map
packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.d.ts
packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js
packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js.map
packages/app-desktop/gui/ShareNoteDialog.d.ts
packages/app-desktop/gui/ShareNoteDialog.js
packages/app-desktop/gui/ShareNoteDialog.js.map
@@ -640,9 +671,15 @@ packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map
packages/app-desktop/gui/style/StyledFormLabel.d.ts
packages/app-desktop/gui/style/StyledFormLabel.js
packages/app-desktop/gui/style/StyledFormLabel.js.map
packages/app-desktop/gui/style/StyledInput.d.ts
packages/app-desktop/gui/style/StyledInput.js
packages/app-desktop/gui/style/StyledInput.js.map
packages/app-desktop/gui/style/StyledMessage.d.ts
packages/app-desktop/gui/style/StyledMessage.js
packages/app-desktop/gui/style/StyledMessage.js.map
packages/app-desktop/gui/style/StyledTextInput.d.ts
packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/style/StyledTextInput.js.map
@@ -841,6 +878,9 @@ packages/lib/InMemoryCache.js.map
packages/lib/JoplinDatabase.d.ts
packages/lib/JoplinDatabase.js
packages/lib/JoplinDatabase.js.map
packages/lib/JoplinError.d.ts
packages/lib/JoplinError.js
packages/lib/JoplinError.js.map
packages/lib/JoplinServerApi.d.ts
packages/lib/JoplinServerApi.js
packages/lib/JoplinServerApi.js.map
@@ -871,6 +911,9 @@ packages/lib/commands/synchronize.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map
packages/lib/debug/DebugService.d.ts
packages/lib/debug/DebugService.js
packages/lib/debug/DebugService.js.map
packages/lib/dummy.test.d.ts
packages/lib/dummy.test.js
packages/lib/dummy.test.js.map
@@ -1327,6 +1370,12 @@ packages/lib/services/searchengine/filterParser.js.map
packages/lib/services/searchengine/queryBuilder.d.ts
packages/lib/services/searchengine/queryBuilder.js
packages/lib/services/searchengine/queryBuilder.js.map
packages/lib/services/share/ShareService.d.ts
packages/lib/services/share/ShareService.js
packages/lib/services/share/ShareService.js.map
packages/lib/services/share/reducer.d.ts
packages/lib/services/share/reducer.js
packages/lib/services/share/reducer.js.map
packages/lib/services/spellChecker/SpellCheckerService.d.ts
packages/lib/services/spellChecker/SpellCheckerService.js
packages/lib/services/spellChecker/SpellCheckerService.js.map

48
.gitignore vendored
View File

@@ -96,6 +96,12 @@ packages/app-cli/tests/Synchronizer.tags.js.map
packages/app-cli/tests/Synchronizer.tools.d.ts
packages/app-cli/tests/Synchronizer.tools.js
packages/app-cli/tests/Synchronizer.tools.js.map
packages/app-cli/tests/dateTimeFormats.d.ts
packages/app-cli/tests/dateTimeFormats.js
packages/app-cli/tests/dateTimeFormats.js.map
packages/app-cli/tests/file-api-driver.d.ts
packages/app-cli/tests/file-api-driver.js
packages/app-cli/tests/file-api-driver.js.map
packages/app-cli/tests/fsDriver.d.ts
packages/app-cli/tests/fsDriver.js
packages/app-cli/tests/fsDriver.js.map
@@ -105,6 +111,9 @@ packages/app-cli/tests/htmlUtils.js.map
packages/app-cli/tests/models_Folder.d.ts
packages/app-cli/tests/models_Folder.js
packages/app-cli/tests/models_Folder.js.map
packages/app-cli/tests/models_Folder.sharing.d.ts
packages/app-cli/tests/models_Folder.sharing.js
packages/app-cli/tests/models_Folder.sharing.js.map
packages/app-cli/tests/models_Note.d.ts
packages/app-cli/tests/models_Note.js
packages/app-cli/tests/models_Note.js.map
@@ -144,6 +153,9 @@ packages/app-cli/tests/services_PluginService.js.map
packages/app-cli/tests/services_ResourceService.d.ts
packages/app-cli/tests/services_ResourceService.js
packages/app-cli/tests/services_ResourceService.js.map
packages/app-cli/tests/services_SearchEngineUtils.d.ts
packages/app-cli/tests/services_SearchEngineUtils.js
packages/app-cli/tests/services_SearchEngineUtils.js.map
packages/app-cli/tests/services_keychainService.d.ts
packages/app-cli/tests/services_keychainService.js
packages/app-cli/tests/services_keychainService.js.map
@@ -174,6 +186,9 @@ packages/app-desktop/app.js.map
packages/app-desktop/bridge.d.ts
packages/app-desktop/bridge.js
packages/app-desktop/bridge.js.map
packages/app-desktop/checkForUpdates.d.ts
packages/app-desktop/checkForUpdates.js
packages/app-desktop/checkForUpdates.js.map
packages/app-desktop/commands/copyDevCommand.d.ts
packages/app-desktop/commands/copyDevCommand.js
packages/app-desktop/commands/copyDevCommand.js.map
@@ -231,6 +246,15 @@ packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js.ma
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.d.ts
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.test.js.map
packages/app-desktop/gui/Dialog.d.ts
packages/app-desktop/gui/Dialog.js
packages/app-desktop/gui/Dialog.js.map
packages/app-desktop/gui/DialogButtonRow.d.ts
packages/app-desktop/gui/DialogButtonRow.js
packages/app-desktop/gui/DialogButtonRow.js.map
packages/app-desktop/gui/DialogTitle.d.ts
packages/app-desktop/gui/DialogTitle.js
packages/app-desktop/gui/DialogTitle.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@@ -318,6 +342,9 @@ packages/app-desktop/gui/MainScreen/commands/showNoteContentProperties.js.map
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.d.ts
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js
packages/app-desktop/gui/MainScreen/commands/showNoteProperties.js.map
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.d.ts
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js
packages/app-desktop/gui/MainScreen/commands/showShareFolderDialog.js.map
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.d.ts
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js
packages/app-desktop/gui/MainScreen/commands/showShareNoteDialog.js.map
@@ -570,6 +597,9 @@ packages/app-desktop/gui/Root_UpgradeSyncTarget.js.map
packages/app-desktop/gui/SearchBar/SearchBar.d.ts
packages/app-desktop/gui/SearchBar/SearchBar.js
packages/app-desktop/gui/SearchBar/SearchBar.js.map
packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.d.ts
packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js
packages/app-desktop/gui/ShareFolderDialog/ShareFolderDialog.js.map
packages/app-desktop/gui/ShareNoteDialog.d.ts
packages/app-desktop/gui/ShareNoteDialog.js
packages/app-desktop/gui/ShareNoteDialog.js.map
@@ -627,9 +657,15 @@ packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map
packages/app-desktop/gui/style/StyledFormLabel.d.ts
packages/app-desktop/gui/style/StyledFormLabel.js
packages/app-desktop/gui/style/StyledFormLabel.js.map
packages/app-desktop/gui/style/StyledInput.d.ts
packages/app-desktop/gui/style/StyledInput.js
packages/app-desktop/gui/style/StyledInput.js.map
packages/app-desktop/gui/style/StyledMessage.d.ts
packages/app-desktop/gui/style/StyledMessage.js
packages/app-desktop/gui/style/StyledMessage.js.map
packages/app-desktop/gui/style/StyledTextInput.d.ts
packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/style/StyledTextInput.js.map
@@ -828,6 +864,9 @@ packages/lib/InMemoryCache.js.map
packages/lib/JoplinDatabase.d.ts
packages/lib/JoplinDatabase.js
packages/lib/JoplinDatabase.js.map
packages/lib/JoplinError.d.ts
packages/lib/JoplinError.js
packages/lib/JoplinError.js.map
packages/lib/JoplinServerApi.d.ts
packages/lib/JoplinServerApi.js
packages/lib/JoplinServerApi.js.map
@@ -858,6 +897,9 @@ packages/lib/commands/synchronize.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map
packages/lib/debug/DebugService.d.ts
packages/lib/debug/DebugService.js
packages/lib/debug/DebugService.js.map
packages/lib/dummy.test.d.ts
packages/lib/dummy.test.js
packages/lib/dummy.test.js.map
@@ -1314,6 +1356,12 @@ packages/lib/services/searchengine/filterParser.js.map
packages/lib/services/searchengine/queryBuilder.d.ts
packages/lib/services/searchengine/queryBuilder.js
packages/lib/services/searchengine/queryBuilder.js.map
packages/lib/services/share/ShareService.d.ts
packages/lib/services/share/ShareService.js
packages/lib/services/share/ShareService.js.map
packages/lib/services/share/reducer.d.ts
packages/lib/services/share/reducer.js
packages/lib/services/share/reducer.js.map
packages/lib/services/spellChecker/SpellCheckerService.d.ts
packages/lib/services/spellChecker/SpellCheckerService.js
packages/lib/services/spellChecker/SpellCheckerService.js.map

77
DEPLOY.md Normal file
View File

@@ -0,0 +1,77 @@
# Deploying Joplin apps and scripts
Various scripts are provided to deploy the Joplin applications, scripts and tools.
## Setting up version numbers
Before new releases are created, all version numbers must be updated. This is done using the `setupNewRelease` script and passing it the new major.minor version number. For example:
npm run setupNewRelease -- 1.8
Patch numbers are going to be incremented automatically when releasing each individual package.
## Desktop application
The desktop application is built for Windows, macOS and Linux via continuous integration, by pushing a version tag to GitHub. The process is automated using:
npm run releaseDesktop
## Android application
The app is built and upload to GitHub using:
npm run releaseAndroid -- --type=prerelease
The "type" parameter can be either "release" or "prerelease"
## iOS application
It must be built and released manually using XCode.
## CLI application
Unlike the mobile or desktop application, the CLI app doesn't bundle its dependencies and is always installed from source. For that reason, all its `@joplin` dependencies must be deployed publicly first. This is done using:
npm run publishAll
This is going to publish all the Joplin libraries, such as `@joplin/lib`, `@joplin/tools`, etc.
Then in `app-cli/package.json`, all `@joplin` dependencies and devdependencies must be set to the last major/minor version. For example:
```json
"dependencies": {
"@joplin/lib": "1.8",
"@joplin/renderer": "1.8",
"...": "..."
},
"devDependencies": {
"@joplin/tools": "1.8",
"...": "..."
}
```
Finally, to release the actual app, run:
npm run releaseCli
## Joplin Server
Run:
npm run releaseServer
## Web clipper
Run:
npm run releaseClipper
## Plugin generator
First the types should generally be updated, using `./updateTypes.sh`. Then run:
npm run releasePluginGenerator
## Plugin Repo Cli
Since it has dependencies to the `@joplin` packages, it is released when running `npm run publishAll`

12
LICENSE
View File

@@ -1,3 +1,15 @@
All code in this repository is licensed under the MIT License **unless a
directory contains a LICENSE file**, in which case that LICENSE file applies to
the code in that sub-directory.
For example, packages/fork-sax contains a ISC LICENSE file, thus all files
under the packages/fork-sax directory are licensed under ISC.
For example, packages/app-cli does NOT contain a LICENSE file, thus all files
under that directory are licensed under the default license, which is MIT.
* * *
MIT License
Copyright (c) 2016-2020 Laurent Cozic

View File

@@ -22,11 +22,11 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download
---|---
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/Joplin-Setup-1.7.11.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/Joplin-1.7.11.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/Joplin-1.7.11.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a>
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Joplin-Setup-1.8.5.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Joplin-1.8.5.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/Joplin-1.8.5.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a>
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v1.7.11/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Windows**, you may also use the <a href='https://github.com/laurent22/joplin/releases/download/v1.8.5/JoplinPortable.exe'>Portable version</a>. The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
**On Linux**, the recommended way is to use the following installation script as it will handle the desktop icon too:
@@ -511,47 +511,47 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
![](https://joplinapp.org/images/flags/country-4x3/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 99%
![](https://joplinapp.org/images/flags/country-4x3/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 98%
![](https://joplinapp.org/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 31%
![](https://joplinapp.org/images/flags/country-4x3/ba.png) | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 74%
![](https://joplinapp.org/images/flags/country-4x3/bg.png) | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 60%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 85%
![](https://joplinapp.org/images/flags/country-4x3/ba.png) | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 76%
![](https://joplinapp.org/images/flags/country-4x3/bg.png) | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 59%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 84%
![](https://joplinapp.org/images/flags/country-4x3/hr.png) | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 99%
![](https://joplinapp.org/images/flags/country-4x3/cz.png) | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 89%
![](https://joplinapp.org/images/flags/country-4x3/dk.png) | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 96%
![](https://joplinapp.org/images/flags/country-4x3/cz.png) | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | [Lukas Helebrandt](mailto:lukas@aiya.cz) | 88%
![](https://joplinapp.org/images/flags/country-4x3/dk.png) | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | Mustafa Al-Dailemi (dailemi@hotmail.com)Language-Team: | 98%
![](https://joplinapp.org/images/flags/country-4x3/de.png) | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 98%
![](https://joplinapp.org/images/flags/country-4x3/ee.png) | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 58%
![](https://joplinapp.org/images/flags/country-4x3/gb.png) | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/us.png) | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
![](https://joplinapp.org/images/flags/country-4x3/es.png) | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Mario Campo](mailto:mario.campo@gmail.com) | 97%
![](https://joplinapp.org/images/flags/esperanto.png) | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 34%
![](https://joplinapp.org/images/flags/esperanto.png) | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 33%
![](https://joplinapp.org/images/flags/country-4x3/fi.png) | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 97%
![](https://joplinapp.org/images/flags/country-4x3/fr.png) | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 95%
![](https://joplinapp.org/images/flags/country-4x3/fr.png) | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 94%
![](https://joplinapp.org/images/flags/es/galicia.png) | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 39%
![](https://joplinapp.org/images/flags/country-4x3/id.png) | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 96%
![](https://joplinapp.org/images/flags/country-4x3/id.png) | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [eresytter](mailto:42007357+eresytter@users.noreply.github.com) | 95%
![](https://joplinapp.org/images/flags/country-4x3/it.png) | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/hu.png) | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Szőke Sándor](mailto:mail@szokesandor.hu) | 91%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 95%
![](https://joplinapp.org/images/flags/country-4x3/nl.png) | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 98%
![](https://joplinapp.org/images/flags/country-4x3/hu.png) | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Szőke Sándor](mailto:mail@szokesandor.hu) | 90%
![](https://joplinapp.org/images/flags/country-4x3/be.png) | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 94%
![](https://joplinapp.org/images/flags/country-4x3/nl.png) | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MetBril](mailto:metbril@users.noreply.github.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/no.png) | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 78%
![](https://joplinapp.org/images/flags/country-4x3/ir.png) | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 74%
![](https://joplinapp.org/images/flags/country-4x3/ir.png) | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 73%
![](https://joplinapp.org/images/flags/country-4x3/pl.png) | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Nicolas Suzuki](mailto:nicolas.suzuki@pm.me) | 97%
![](https://joplinapp.org/images/flags/country-4x3/pt.png) | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 68%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 99%
![](https://joplinapp.org/images/flags/country-4x3/si.png) | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 98%
![](https://joplinapp.org/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 63%
![](https://joplinapp.org/images/flags/country-4x3/th.png) | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 47%
![](https://joplinapp.org/images/flags/country-4x3/th.png) | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 46%
![](https://joplinapp.org/images/flags/country-4x3/vi.png) | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
![](https://joplinapp.org/images/flags/country-4x3/tr.png) | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/ua.png) | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/gr.png) | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 85%
![](https://joplinapp.org/images/flags/country-4x3/gr.png) | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 84%
![](https://joplinapp.org/images/flags/country-4x3/ru.png) | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/rs.png) | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 73%
![](https://joplinapp.org/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [Yang Zhang](mailto:zyangmath@gmail.com) | 97%
![](https://joplinapp.org/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 95%
![](https://joplinapp.org/images/flags/country-4x3/jp.png) | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 98%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 97%
![](https://joplinapp.org/images/flags/country-4x3/kr.png) | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 99%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributors

View File

@@ -841,16 +841,6 @@ async function fetchAllNotes() {
<td>int</td>
<td></td>
</tr>
<tr>
<td>is_linked_folder</td>
<td>int</td>
<td></td>
</tr>
<tr>
<td>source_folder_owner_id</td>
<td>text</td>
<td></td>
</tr>
</tbody>
</table>
<h2>GET /folders<a name="get-folders" href="#get-folders" class="heading-anchor">🔗</a></h2>

View File

@@ -405,6 +405,22 @@ https://github.com/laurent22/joplin/blob/dev/readme/changelog_cli.md
<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&amp;business=E8JMYD2LQ8MMA&amp;lc=GB&amp;item_name=Joplin+Development&amp;currency_code=EUR&amp;bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted"><img src="https://joplinapp.org/images/badges/Donate-PayPal-green.svg" alt="Donate using PayPal"></a> <a href="https://github.com/sponsors/laurent22/"><img src="https://joplinapp.org/images/badges/GitHub-Badge.svg" alt="Sponsor on GitHub"></a> <a href="https://www.patreon.com/joplin"><img src="https://joplinapp.org/images/badges/Patreon-Badge.svg" alt="Become a patron"></a> <a href="https://joplinapp.org/donate/#donations"><img src="https://joplinapp.org/images/badges/Donate-IBAN.svg" alt="Donate using IBAN"></a></p>
<hr>
<h1>Joplin terminal app changelog<a name="joplin-terminal-app-changelog" href="#joplin-terminal-app-changelog" class="heading-anchor">🔗</a></h1>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/cli-v1.8.1">cli-v1.8.1</a> - 2021-05-10T09:38:05Z<a name="cli-v1-8-1-https-github-com-laurent22-joplin-releases-tag-cli-v1-8-1-2021-05-10t09-38-05z" href="#cli-v1-8-1-https-github-com-laurent22-joplin-releases-tag-cli-v1-8-1-2021-05-10t09-38-05z" class="heading-anchor">🔗</a></h2>
<ul>
<li>New: Add &quot;id&quot; and &quot;due&quot; search filters (#4898 by <a href="https://github.com/JackGruber">@JackGruber</a>)</li>
<li>New: Add support for &quot;batch&quot; command (eef86d6)</li>
<li>Improved: Also duplicate the tags when the note is duplicated (#4876) (#3157 by <a href="https://github.com/JackGruber">@JackGruber</a>)</li>
<li>Improved: Bump KaTeX to 0.13.3 (#4902 by Roman Musin)</li>
<li>Improved: Filter &quot;notebook&quot; can now be negated (#4651 by Naveen M V)</li>
<li>Improved: Improved error handling when importing ENEX (257cde4)</li>
<li>Improved: Save user settings to JSON file (71f976f)</li>
<li>Improved: Some imported ENEX files incorrectly had invisible sections (f7a457f)</li>
<li>Fixed: Disable WebDAV response caching (#4887) (#4706 by Roman Musin)</li>
<li>Fixed: Fixed issue when getting version info (54884d6)</li>
<li>Fixed: Fixed rendering of note and resource links (61399ce)</li>
<li>Fixed: Regression: Fixed network request repeat mechanism (ede6004)</li>
<li>Security: Apply npm audit security fixes (0b67446)</li>
</ul>
<h2><a href="https://github.com/laurent22/joplin/releases/tag/cli-v1.6.4">cli-v1.6.4</a> - 2021-01-21T10:01:15Z<a name="cli-v1-6-4-https-github-com-laurent22-joplin-releases-tag-cli-v1-6-4-2021-01-21t10-01-15z" href="#cli-v1-6-4-https-github-com-laurent22-joplin-releases-tag-cli-v1-6-4-2021-01-21t10-01-15z" class="heading-anchor">🔗</a></h2>
<ul>
<li>Fixed: Fixed infinite sync issue with OneDrive (#4305)</li>

View File

@@ -455,6 +455,13 @@ notepad++.exe --openSession # Opens Notepad ++ in new window
<p>You may use a special keyboard such as <a href="https://play.google.com/store/apps/details?id=kl.ime.oh&amp;hl=en">Multiling O Keyboard</a>, which has shortcuts to create Markdown tags. <a href="https://discourse.joplinapp.org/t/android-create-new-list-item-with-enter/585/2?u=laurent">More information in this post</a>.</p>
<h2>The initial sync is very slow, how can I speed it up?<a name="the-initial-sync-is-very-slow-how-can-i-speed-it-up" href="#the-initial-sync-is-very-slow-how-can-i-speed-it-up" class="heading-anchor">🔗</a></h2>
<p>Whenever importing a large number of notes, for example from Evernote, it may take a very long time for the first sync to complete. There are various techniques to speed thing up (if you don't want to simply wait for the sync to complete), which are outlined in <a href="https://discourse.joplinapp.org/t/workaround-for-slow-initial-bulk-sync-after-evernote-import/746?u=laurent">this post</a>.</p>
<h2>Not all notes, folders, or tags are displayed on the mobile app<a name="not-all-notes-folders-or-tags-are-displayed-on-the-mobile-app" href="#not-all-notes-folders-or-tags-are-displayed-on-the-mobile-app" class="heading-anchor">🔗</a></h2>
<p>Joplin does not have a background sync on mobile devices. When Joplin is closed, sent to the background or the device is put into sleep (display off), the sync is interrupted.</p>
<h2>How can I check the sync status?<a name="how-can-i-check-the-sync-status" href="#how-can-i-check-the-sync-status" class="heading-anchor">🔗</a></h2>
<p>Go to the synchronisation page. You can find it on the desktop application under <code>Help &gt; Synchronisation Status</code> and on the mobile app under <code>Configuration &gt; SYNC STATUS</code>.</p>
<p><code>total items</code> = How many items there are in total to sync.<br>
<code>synced items</code> = How many items have already been uploaded or downloaded.</p>
<p>If <code>total items</code> and <code>synced items</code> are equal, all data has been synced. Also all devices should have the same <code>total items</code>.</p>
<h2>Is it possible to use real file and folder names in the sync target?<a name="is-it-possible-to-use-real-file-and-folder-names-in-the-sync-target" href="#is-it-possible-to-use-real-file-and-folder-names-in-the-sync-target" class="heading-anchor">🔗</a></h2>
<p>Unfortunately it is not possible. Joplin synchronises with file systems using an open format however it does not mean the sync files are meant to be user-editable. The format is designed to be performant and reliable, not user friendly (it cannot be both), and that cannot be changed. Joplin sync directory is basically just a database.</p>
<h2>Could there be a password to restrict access to Joplin?<a name="could-there-be-a-password-to-restrict-access-to-joplin" href="#could-there-be-a-password-to-restrict-access-to-joplin" class="heading-anchor">🔗</a></h2>

View File

@@ -17,7 +17,7 @@
"sync.target": {
"type": "integer",
"default": 7,
"description": "Synchronisationsziel",
"description": "Synchronisation target",
"enum": [
2,
3,
@@ -41,44 +41,44 @@
"sync.2.path": {
"type": "string",
"default": "",
"description": "Verzeichnis, mit dem synchronisiert werden soll (absoluter Pfad). Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
"description": "Directory to synchronise with (absolute path). Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
},
"sync.5.path": {
"type": "string",
"default": "",
"description": "Nextcloud-WebDAV-URL. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
"description": "Nextcloud WebDAV URL. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
},
"sync.5.username": {
"type": "string",
"default": "",
"description": "Nextcloud-Benutzername"
"description": "Nextcloud username"
},
"sync.5.password": {
"type": "string",
"default": "",
"description": "Nextcloud-Passwort",
"description": "Nextcloud password",
"$comment": "private"
},
"sync.6.path": {
"type": "string",
"default": "",
"description": "WebDAV-URL. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
"description": "WebDAV URL. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
},
"sync.6.username": {
"type": "string",
"default": "",
"description": "WebDAV-Benutzername"
"description": "WebDAV username"
},
"sync.6.password": {
"type": "string",
"default": "",
"description": "WebDAV-Passwort",
"description": "WebDAV password",
"$comment": "private"
},
"sync.8.path": {
"type": "string",
"default": "",
"description": "Amazon S3-Bucket. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
"description": "AWS S3 bucket. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
},
"sync.8.url": {
"type": "string",
@@ -88,33 +88,33 @@
"sync.8.username": {
"type": "string",
"default": "",
"description": "AWS-Schlüssel"
"description": "AWS key"
},
"sync.8.password": {
"type": "string",
"default": "",
"description": "AWS-Geheimnis",
"description": "AWS secret",
"$comment": "private"
},
"sync.9.path": {
"type": "string",
"default": "",
"description": "Joplin-Server-URL. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
"description": "Joplin Server URL. Attention: If you change this location, make sure you copy all your content to it before syncing, otherwise all files will be removed! See the FAQ for more details: https://joplinapp.org/faq/"
},
"sync.9.directory": {
"type": "string",
"default": "Apps/Joplin",
"description": "Joplin-Server-Verzeichnis"
"description": "Joplin Server Directory"
},
"sync.9.username": {
"type": "string",
"default": "",
"description": "Joplin-Server-Benutzername"
"description": "Joplin Server username"
},
"sync.9.password": {
"type": "string",
"default": "",
"description": "Joplin-Server-Passwort",
"description": "Joplin Server password",
"$comment": "private"
},
"sync.5.syncTargets": {
@@ -125,7 +125,7 @@
"sync.resourceDownloadMode": {
"type": "string",
"default": "always",
"description": "Verhalten für das Herunterladen von Anhängen. Im Modus „Manuell“ werden die Anhänge nur heruntergeladen, wenn du auf sie klickst. Bei „Automatisch“ werden sie heruntergeladen, sobald du die Notiz öffnest. Bei „Immer“ werden die Anhänge heruntergeladen, egal ob du die Notiz öffnest oder nicht.",
"description": "Attachment download behaviour. In \"Manual\" mode, attachments are downloaded only when you click on them. In \"Auto\", they are downloaded when you open the note. In \"Always\", all the attachments are downloaded whether you open the note or not.",
"enum": [
"always",
"manual",
@@ -200,7 +200,7 @@
"sync.maxConcurrentConnections": {
"type": "integer",
"default": 5,
"description": "Maximale Anzahl an gleichzeitigen Verbindungen",
"description": "Max concurrent connections",
"minimum": 1,
"maximum": 20
},
@@ -222,7 +222,7 @@
"locale": {
"type": "string",
"default": "en_GB",
"description": "Sprache",
"description": "Language",
"enum": [
"ar",
"eu",
@@ -270,7 +270,7 @@
"dateFormat": {
"type": "string",
"default": "DD/MM/YYYY",
"description": "Datumsformat",
"description": "Date format",
"enum": [
"DD/MM/YYYY",
"DD/MM/YY",
@@ -278,13 +278,14 @@
"MM/DD/YY",
"YYYY-MM-DD",
"DD.MM.YYYY",
"YYYY.MM.DD"
"YYYY.MM.DD",
"YYMMDD"
]
},
"timeFormat": {
"type": "string",
"default": "HH:mm",
"description": "Zeitformat",
"description": "Time format",
"enum": [
"HH:mm",
"h:mm A"
@@ -293,7 +294,7 @@
"theme": {
"type": "integer",
"default": 1,
"description": "Design",
"description": "Theme",
"enum": [
1,
2,
@@ -308,12 +309,12 @@
"themeAutoDetect": {
"type": "boolean",
"default": false,
"description": "Automatisch das Design ändern, um es dem System-Design anzupassen"
"description": "Automatically switch theme to match system theme"
},
"preferredLightTheme": {
"type": "integer",
"default": 1,
"description": "Bevorzugtes helles Design",
"description": "Preferred light theme",
"enum": [
1,
2,
@@ -328,7 +329,7 @@
"preferredDarkTheme": {
"type": "integer",
"default": 2,
"description": "Bevorzugtes dunkles Design",
"description": "Preferred dark theme",
"enum": [
1,
2,
@@ -348,7 +349,7 @@
"showNoteCounts": {
"type": "boolean",
"default": true,
"description": "Notizanzahl anzeigen",
"description": "Show note counts",
"$comment": "private"
},
"layoutButtonSequence": {
@@ -365,17 +366,17 @@
"uncompletedTodosOnTop": {
"type": "boolean",
"default": true,
"description": "Unvollständige Aufgaben oben"
"description": "Uncompleted to-dos on top"
},
"showCompletedTodos": {
"type": "boolean",
"default": true,
"description": "Abgeschlossene Aufgaben anzeigen"
"description": "Show completed to-dos"
},
"notes.sortOrder.field": {
"type": "string",
"default": "user_updated_time",
"description": "Sortiere Notizen nach",
"description": "Sort notes by",
"enum": [
"user_updated_time",
"user_created_time",
@@ -386,17 +387,17 @@
"editor.autoMatchingBraces": {
"type": "boolean",
"default": true,
"description": "Automatisches Hinzufügen von geschweiften Klammern, runden Klammern, Anführungszeichen usw."
"description": "Auto-pair braces, parenthesis, quotations, etc."
},
"notes.sortOrder.reverse": {
"type": "boolean",
"default": true,
"description": "Sortierreihenfolge umkehren"
"description": "Reverse sort order"
},
"folders.sortOrder.field": {
"type": "string",
"default": "title",
"description": "Notizbücher sortieren nach",
"description": "Sort notebooks by",
"enum": [
"title",
"last_note_user_updated_time"
@@ -405,12 +406,12 @@
"folders.sortOrder.reverse": {
"type": "boolean",
"default": false,
"description": "Sortierreihenfolge umkehren"
"description": "Reverse sort order"
},
"trackLocation": {
"type": "boolean",
"default": true,
"description": "Momentanen Standort zusammen mit Notizen speichern"
"description": "Save geo-location with notes"
},
"editor.beta": {
"type": "boolean",
@@ -421,7 +422,7 @@
"newTodoFocus": {
"type": "string",
"default": "title",
"description": "Wenn eine neue Aufgabe erstellt wird:",
"description": "When creating a new to-do:",
"enum": [
"title",
"body"
@@ -430,7 +431,7 @@
"newNoteFocus": {
"type": "string",
"default": "body",
"description": "Wenn eine neue Notiz erstellt wird:",
"description": "When creating a new note:",
"enum": [
"title",
"body"
@@ -459,107 +460,107 @@
"markdown.plugin.softbreaks": {
"type": "boolean",
"default": false,
"description": "Weiche Zeilenumbrüche aktivieren"
"description": "Enable soft breaks"
},
"markdown.plugin.typographer": {
"type": "boolean",
"default": false,
"description": "Typographie-Unterstützung aktivieren"
"description": "Enable typographer support"
},
"markdown.plugin.linkify": {
"type": "boolean",
"default": true,
"description": "Linkify aktivieren"
"description": "Enable Linkify"
},
"markdown.plugin.katex": {
"type": "boolean",
"default": true,
"description": "Mathematische Ausdrücke aktivieren"
"description": "Enable math expressions"
},
"markdown.plugin.fountain": {
"type": "boolean",
"default": false,
"description": "Fountain-Syntaxunterstützung aktivieren"
"description": "Enable Fountain syntax support"
},
"markdown.plugin.mermaid": {
"type": "boolean",
"default": true,
"description": "Mermaid-Diagrammunterstützung aktivieren"
"description": "Enable Mermaid diagrams support"
},
"markdown.plugin.audioPlayer": {
"type": "boolean",
"default": true,
"description": "Audiospieler aktivieren"
"description": "Enable audio player"
},
"markdown.plugin.videoPlayer": {
"type": "boolean",
"default": true,
"description": "Videospieler aktivieren"
"description": "Enable video player"
},
"markdown.plugin.pdfViewer": {
"type": "boolean",
"default": true,
"description": "PDF-Betrachter aktivieren"
"description": "Enable PDF viewer"
},
"markdown.plugin.mark": {
"type": "boolean",
"default": true,
"description": "Syntax ==mark== aktivieren"
"description": "Enable ==mark== syntax"
},
"markdown.plugin.footnote": {
"type": "boolean",
"default": true,
"description": "Fußnoten aktivieren"
"description": "Enable footnotes"
},
"markdown.plugin.toc": {
"type": "boolean",
"default": true,
"description": "Inhaltsverzeichnis-Erweiterung aktivieren"
"description": "Enable table of contents extension"
},
"markdown.plugin.sub": {
"type": "boolean",
"default": false,
"description": "Syntax ~sub~ aktivieren"
"description": "Enable ~sub~ syntax"
},
"markdown.plugin.sup": {
"type": "boolean",
"default": false,
"description": "Syntax ^sup^ aktivieren"
"description": "Enable ^sup^ syntax"
},
"markdown.plugin.deflist": {
"type": "boolean",
"default": false,
"description": "Syntax deflist aktivieren"
"description": "Enable deflist syntax"
},
"markdown.plugin.abbr": {
"type": "boolean",
"default": false,
"description": "Abkürzungssyntax aktivieren"
"description": "Enable abbreviation syntax"
},
"markdown.plugin.emoji": {
"type": "boolean",
"default": false,
"description": "Markdown Emoji aktivieren"
"description": "Enable markdown emoji"
},
"markdown.plugin.insert": {
"type": "boolean",
"default": false,
"description": "Syntax ++insert++ aktivieren"
"description": "Enable ++insert++ syntax"
},
"markdown.plugin.multitable": {
"type": "boolean",
"default": false,
"description": "Multimarkdown Tabellenerweiterung aktivieren"
"description": "Enable multimarkdown table extension"
},
"showTrayIcon": {
"type": "boolean",
"default": true,
"description": "Taskleistensymbol anzeigen. Dadurch kann Joplin im Hintergrund laufen. Es wird empfohlen, diese Einstellung zu aktivieren, damit deine Notizen ständig synchronisiert werden und somit die Anzahl der Konflikte reduziert wird."
"description": "Show tray icon. This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts."
},
"startMinimized": {
"type": "boolean",
"default": false,
"description": "Anwendung minimiert als Taskleistensymbol starten"
"description": "Start application minimised in the tray icon"
},
"collapsedFolderIds": {
"type": "array",
@@ -611,19 +612,19 @@
"style.editor.fontSize": {
"type": "integer",
"default": 13,
"description": "Schriftgröße im Editor",
"description": "Editor font size",
"minimum": 4,
"maximum": 50
},
"style.editor.fontFamily": {
"type": "string",
"default": "",
"description": "Schriftfamilie im Editor. Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used."
"description": "Editor font family. Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used."
},
"style.editor.monospaceFontFamily": {
"type": "string",
"default": "",
"description": "Nichtproportionale Schriftfamilie im Editor. Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used."
"description": "Editor monospace font family. Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used."
},
"ui.layout": {
"type": "object",
@@ -633,12 +634,12 @@
"autoUpdateEnabled": {
"type": "boolean",
"default": false,
"description": "Die Anwendung automatisch aktualisieren"
"description": "Automatically update the application"
},
"autoUpdate.includePreReleases": {
"type": "boolean",
"default": false,
"description": "Bei der Suche nach Aktualisierungen Vorabveröffentlichungen erhalten. Weitere Informationen findest Du auf der Vorabversionsseite: https://joplinapp.org/prereleases"
"description": "Get pre-releases when checking for updates. See the pre-release page for more details: https://joplinapp.org/prereleases"
},
"clipperServer.autoStart": {
"type": "boolean",
@@ -648,7 +649,7 @@
"sync.interval": {
"type": "integer",
"default": 300,
"description": "Synchronisationsintervall",
"description": "Synchronisation interval",
"enum": [
0,
300,
@@ -662,7 +663,7 @@
"sync.mobileWifiOnly": {
"type": "boolean",
"default": false,
"description": "Nur über WiFi-Verbindung synchronisieren"
"description": "Synchronise only over WiFi connection"
},
"noteVisiblePanes": {
"type": "array",
@@ -685,12 +686,12 @@
"editor": {
"type": "string",
"default": "",
"description": "Texteditor-Befehl. Der Editor-Befehl (kann Kommandozeilenargumente enthalten), der zum Öffnen einer Notiz verwendet wird. Wenn keiner angegeben wird, wird versucht, den Standard-Editor automatisch zu erkennen."
"description": "Text editor command. The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor."
},
"export.pdfPageSize": {
"type": "string",
"default": "A4",
"description": "Seitengröße für den PDF-Export",
"description": "Page size for PDF export",
"enum": [
"A4",
"Letter",
@@ -703,7 +704,7 @@
"export.pdfPageOrientation": {
"type": "string",
"default": "portrait",
"description": "Seitenausrichtung für den PDF-Export",
"description": "Page orientation for PDF export",
"enum": [
"portrait",
"landscape"
@@ -712,7 +713,7 @@
"editor.keyboardMode": {
"type": "string",
"default": "",
"description": "Tastatur-Modus",
"description": "Keyboard Mode",
"enum": [
"",
"emacs",
@@ -725,20 +726,26 @@
"description": "Enable spell checking in Markdown editor? (WARNING BETA feature). Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)",
"$comment": "private"
},
"image.noresizing": {
"type": "boolean",
"default": false,
"description": "Do not resize images",
"$comment": "private"
},
"net.customCertificates": {
"type": "string",
"default": "",
"description": "Benutzerdefinierte TLS-Zertifikate. Kommagetrennte Liste von Pfaden zu Verzeichnissen, aus denen die Zertifikate geladen werden, oder Pfad zu einzelnen Zertifikatsdateien. Zum Beispiel: /my/cert_dir, /other/custom.pem. Wenn du Änderungen an den TLS-Einstellungen vornimmst, musst du deine Änderungen speichern, bevor du auf „Synchronisierungskonfiguration prüfen“ klickst."
"description": "Custom TLS certificates. Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on \"Check synchronisation configuration\"."
},
"net.ignoreTlsErrors": {
"type": "boolean",
"default": false,
"description": "TLS-Zertifikatfehler ignorieren"
"description": "Ignore TLS certificate errors"
},
"sync.wipeOutFailSafe": {
"type": "boolean",
"default": true,
"description": "Ausfallsicher. Ausfallsicher: Lösche nicht die lokalen Daten, wenn das Synchronisationsziel leer ist (oft ein Resultat von Fehlkonfiguration oder einem Programmfehler)"
"description": "Fail-safe. Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)"
},
"api.token": {
"type": "string",
@@ -748,7 +755,7 @@
"api.port": {
"type": "integer",
"default": null,
"description": "Spezifiziere den Port, der vom API-Server verwendet werden soll. Wenn er nicht gesetzt ist, wird ein Standardwert verwendet."
"description": "Specify the port that should be used by the API server. If not set, a default will be used."
},
"resourceService.lastProcessedChangeId": {
"type": "integer",
@@ -773,12 +780,12 @@
"revisionService.enabled": {
"type": "boolean",
"default": true,
"description": "Notizenverlauf aktivieren"
"description": "Enable note history"
},
"revisionService.ttlDays": {
"type": "integer",
"default": 90,
"description": "Notizenverlauf speichern für",
"description": "Keep note history for",
"minimum": 1,
"maximum": 730
},
@@ -832,17 +839,17 @@
"layout.folderList.factor": {
"type": "integer",
"default": 1,
"description": "Notizbuch-Listenwachstumsfaktor. Die Faktor-Eigenschaft legt fest, wie der Artikel wächst oder schrumpft, um dem verfügbaren Platz in seinem Container in Bezug auf die anderen Artikel zu entsprechen. Ein Element mit dem Faktor 2 benötigt also doppelt so viel Platz wie ein Element mit dem Faktor 1. Starten Sie die App neu, um Änderungen zu sehen."
"description": "Notebook list growth factor. The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes."
},
"layout.noteList.factor": {
"type": "integer",
"default": 1,
"description": "Notiz-Listenwachstumsfaktor. Die Faktor-Eigenschaft legt fest, wie der Artikel wächst oder schrumpft, um dem verfügbaren Platz in seinem Container in Bezug auf die anderen Artikel zu entsprechen. Ein Element mit dem Faktor 2 benötigt also doppelt so viel Platz wie ein Element mit dem Faktor 1. Starten Sie die App neu, um Änderungen zu sehen."
"description": "Note list growth factor. The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes."
},
"layout.note.factor": {
"type": "integer",
"default": 2,
"description": "Notiz-Flächenwachstumsfaktor. Die Faktor-Eigenschaft legt fest, wie der Artikel wächst oder schrumpft, um dem verfügbaren Platz in seinem Container in Bezug auf die anderen Artikel zu entsprechen. Ein Element mit dem Faktor 2 benötigt also doppelt so viel Platz wie ein Element mit dem Faktor 1. Starten Sie die App neu, um Änderungen zu sehen."
"description": "Note area growth factor. The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes."
},
"isSafeMode": {
"type": "boolean",

View File

@@ -832,7 +832,8 @@ Possible keys/values:
DD/MM/YY (30/01/17), MM/DD/YYYY
(01/30/2017), MM/DD/YY (01/30/17),
YYYY-MM-DD (2017-01-30), DD.MM.YYYY
(30.01.2017), YYYY.MM.DD (2017.01.30).
(30.01.2017), YYYY.MM.DD (2017.01.30),
YYMMDD (170130).
Default: &quot;DD/MM/YYYY&quot;
timeFormat Time format.

View File

@@ -8,9 +8,8 @@
"license": "MIT",
"scripts": {
"audit": "lerna-audit",
"bootstrap": "lerna bootstrap --no-ci",
"bootstrapIgnoreScripts": "lerna bootstrap --ignore-scripts --no-ci",
"bootstrapServerOnly": "lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/server",
"bootstrap": "lerna bootstrap --force-local --no-ci",
"bootstrapServerOnly": "lerna bootstrap --force-local --no-ci --include-dependents --include-dependencies --scope @joplin/server",
"build": "lerna run build && npm run tsc",
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
"buildDoc": "./packages/tools/build-all.sh",

View File

@@ -35,7 +35,6 @@ module.exports = {
'<rootDir>/build/',
'<rootDir>/tests/test-utils.js',
'<rootDir>/tests/test-utils-synchronizer.js',
'<rootDir>/tests/file_api_driver.js',
'<rootDir>/tests/tmp/',
'<rootDir>/tests/test data/',
],

View File

@@ -31,7 +31,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.8.1",
"version": "2.0.0",
"bin": {
"joplin": "./main.js"
},

View File

@@ -9,6 +9,7 @@ import Resource from '@joplin/lib/models/Resource';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import MasterKey from '@joplin/lib/models/MasterKey';
import BaseItem from '@joplin/lib/models/BaseItem';
import { createFolderTree } from './test-utils';
let insideBeforeEach = false;
@@ -361,13 +362,26 @@ describe('Synchronizer.e2ee', function() {
expect((await decryptionWorker().decryptionDisabledItems()).length).toBe(0);
}));
it('should not encrypt notes that are shared', (async () => {
it('should not encrypt notes that are shared by link', (async () => {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
const folder1 = await Folder.save({ title: 'folder1' });
const note1 = await Note.save({ title: 'un', parent_id: folder1.id });
let note2 = await Note.save({ title: 'deux', parent_id: folder1.id });
await createFolderTree('', [
{
title: 'folder1',
children: [
{
title: 'un',
},
{
title: 'deux',
},
],
},
]);
const note1 = await Note.loadByTitle('un');
let note2 = await Note.loadByTitle('deux');
await synchronizerStart();
await switchClient(2);
@@ -400,4 +414,61 @@ describe('Synchronizer.e2ee', function() {
expect(note1_2.title).toBe('');
}));
it('should not encrypt items that are shared by folder', (async () => {
Setting.setValue('encryption.enabled', true);
await loadEncryptionMasterKey();
const folder1 = await createFolderTree('', [
{
title: 'folder1',
children: [
{
title: 'note1',
},
],
},
{
title: 'folder2',
children: [
{
title: 'note2',
},
],
},
]);
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
await switchClient(1);
// Simulate that the folder has been shared
await Folder.save({ id: folder1.id, share_id: 'abcd' });
await synchronizerStart();
await switchClient(2);
await synchronizerStart();
// The shared items should be decrypted
{
const folder1 = await Folder.loadByTitle('folder1');
const note1 = await Note.loadByTitle('note1');
expect(folder1.title).toBe('folder1');
expect(note1.title).toBe('note1');
}
// The non-shared items should be encrypted
{
const folder2 = await Folder.loadByTitle('folder2');
const note2 = await Note.loadByTitle('note2');
expect(folder2).toBeFalsy();
expect(note2).toBeFalsy();
}
}));
});

View File

@@ -0,0 +1,290 @@
'use strict';
const __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); }
return new (P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, '__esModule', { value: true });
const Setting_1 = require('@joplin/lib/models/Setting');
const test_utils_synchronizer_1 = require('./test-utils-synchronizer');
const { syncTargetName, afterAllCleanUp, synchronizerStart, setupDatabaseAndSynchronizer, synchronizer, sleep, switchClient, syncTargetId, fileApi } = require('./test-utils.js');
const Folder_1 = require('@joplin/lib/models/Folder');
const Note_1 = require('@joplin/lib/models/Note');
const BaseItem_1 = require('@joplin/lib/models/BaseItem');
const WelcomeUtils = require('@joplin/lib/WelcomeUtils');
describe('Synchronizer.basics', function() {
beforeEach((done) => __awaiter(this, void 0, void 0, function* () {
yield setupDatabaseAndSynchronizer(1);
yield setupDatabaseAndSynchronizer(2);
yield switchClient(1);
done();
}));
afterAll(() => __awaiter(this, void 0, void 0, function* () {
yield afterAllCleanUp();
}));
it('should create remote items', (() => __awaiter(this, void 0, void 0, function* () {
const folder = yield Folder_1.default.save({ title: 'folder1' });
yield Note_1.default.save({ title: 'un', parent_id: folder.id });
const all = yield test_utils_synchronizer_1.allNotesFolders();
yield synchronizerStart();
yield test_utils_synchronizer_1.localNotesFoldersSameAsRemote(all, expect);
})));
it('should update remote items', (() => __awaiter(this, void 0, void 0, function* () {
const folder = yield Folder_1.default.save({ title: 'folder1' });
const note = yield Note_1.default.save({ title: 'un', parent_id: folder.id });
yield synchronizerStart();
yield Note_1.default.save({ title: 'un UPDATE', id: note.id });
const all = yield test_utils_synchronizer_1.allNotesFolders();
yield synchronizerStart();
yield test_utils_synchronizer_1.localNotesFoldersSameAsRemote(all, expect);
})));
it('should create local items', (() => __awaiter(this, void 0, void 0, function* () {
const folder = yield Folder_1.default.save({ title: 'folder1' });
yield Note_1.default.save({ title: 'un', parent_id: folder.id });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
const all = yield test_utils_synchronizer_1.allNotesFolders();
yield test_utils_synchronizer_1.localNotesFoldersSameAsRemote(all, expect);
})));
it('should update local items', (() => __awaiter(this, void 0, void 0, function* () {
const folder1 = yield Folder_1.default.save({ title: 'folder1' });
const note1 = yield Note_1.default.save({ title: 'un', parent_id: folder1.id });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield sleep(0.1);
let note2 = yield Note_1.default.load(note1.id);
note2.title = 'Updated on client 2';
yield Note_1.default.save(note2);
note2 = yield Note_1.default.load(note2.id);
yield synchronizerStart();
yield switchClient(1);
yield synchronizerStart();
const all = yield test_utils_synchronizer_1.allNotesFolders();
yield test_utils_synchronizer_1.localNotesFoldersSameAsRemote(all, expect);
})));
it('should delete remote notes', (() => __awaiter(this, void 0, void 0, function* () {
const folder1 = yield Folder_1.default.save({ title: 'folder1' });
const note1 = yield Note_1.default.save({ title: 'un', parent_id: folder1.id });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield sleep(0.1);
yield Note_1.default.delete(note1.id);
yield synchronizerStart();
const remotes = yield test_utils_synchronizer_1.remoteNotesAndFolders();
expect(remotes.length).toBe(1);
expect(remotes[0].id).toBe(folder1.id);
const deletedItems = yield BaseItem_1.default.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
})));
it('should not created deleted_items entries for items deleted via sync', (() => __awaiter(this, void 0, void 0, function* () {
const folder1 = yield Folder_1.default.save({ title: 'folder1' });
yield Note_1.default.save({ title: 'un', parent_id: folder1.id });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield Folder_1.default.delete(folder1.id);
yield synchronizerStart();
yield switchClient(1);
yield synchronizerStart();
const deletedItems = yield BaseItem_1.default.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
})));
it('should delete local notes', (() => __awaiter(this, void 0, void 0, function* () {
const folder1 = yield Folder_1.default.save({ title: 'folder1' });
const note1 = yield Note_1.default.save({ title: 'un', parent_id: folder1.id });
const note2 = yield Note_1.default.save({ title: 'deux', parent_id: folder1.id });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield Note_1.default.delete(note1.id);
yield synchronizerStart();
yield switchClient(1);
yield synchronizerStart();
const items = yield test_utils_synchronizer_1.allNotesFolders();
expect(items.length).toBe(2);
const deletedItems = yield BaseItem_1.default.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
yield Note_1.default.delete(note2.id);
yield synchronizerStart();
})));
it('should delete remote folder', (() => __awaiter(this, void 0, void 0, function* () {
yield Folder_1.default.save({ title: 'folder1' });
const folder2 = yield Folder_1.default.save({ title: 'folder2' });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield sleep(0.1);
yield Folder_1.default.delete(folder2.id);
yield synchronizerStart();
const all = yield test_utils_synchronizer_1.allNotesFolders();
yield test_utils_synchronizer_1.localNotesFoldersSameAsRemote(all, expect);
})));
it('should delete local folder', (() => __awaiter(this, void 0, void 0, function* () {
yield Folder_1.default.save({ title: 'folder1' });
const folder2 = yield Folder_1.default.save({ title: 'folder2' });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield Folder_1.default.delete(folder2.id);
yield synchronizerStart();
yield switchClient(1);
yield synchronizerStart();
const items = yield test_utils_synchronizer_1.allNotesFolders();
yield test_utils_synchronizer_1.localNotesFoldersSameAsRemote(items, expect);
})));
it('should cross delete all folders', (() => __awaiter(this, void 0, void 0, function* () {
// If client1 and 2 have two folders, client 1 deletes item 1 and client
// 2 deletes item 2, they should both end up with no items after sync.
const folder1 = yield Folder_1.default.save({ title: 'folder1' });
const folder2 = yield Folder_1.default.save({ title: 'folder2' });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield sleep(0.1);
yield Folder_1.default.delete(folder1.id);
yield switchClient(1);
yield Folder_1.default.delete(folder2.id);
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
const items2 = yield test_utils_synchronizer_1.allNotesFolders();
yield switchClient(1);
yield synchronizerStart();
const items1 = yield test_utils_synchronizer_1.allNotesFolders();
expect(items1.length).toBe(0);
expect(items1.length).toBe(items2.length);
})));
it('items should be downloaded again when user cancels in the middle of delta operation', (() => __awaiter(this, void 0, void 0, function* () {
const folder1 = yield Folder_1.default.save({ title: 'folder1' });
yield Note_1.default.save({ title: 'un', is_todo: 1, parent_id: folder1.id });
yield synchronizerStart();
yield switchClient(2);
synchronizer().testingHooks_ = ['cancelDeltaLoop2'];
yield synchronizerStart();
let notes = yield Note_1.default.all();
expect(notes.length).toBe(0);
synchronizer().testingHooks_ = [];
yield synchronizerStart();
notes = yield Note_1.default.all();
expect(notes.length).toBe(1);
})));
it('should skip items that cannot be synced', (() => __awaiter(this, void 0, void 0, function* () {
const folder1 = yield Folder_1.default.save({ title: 'folder1' });
const note1 = yield Note_1.default.save({ title: 'un', is_todo: 1, parent_id: folder1.id });
const noteId = note1.id;
yield synchronizerStart();
let disabledItems = yield BaseItem_1.default.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
yield Note_1.default.save({ id: noteId, title: 'un mod' });
synchronizer().testingHooks_ = ['notesRejectedByTarget'];
yield synchronizerStart();
synchronizer().testingHooks_ = [];
yield synchronizerStart(); // Another sync to check that this item is now excluded from sync
yield switchClient(2);
yield synchronizerStart();
const notes = yield Note_1.default.all();
expect(notes.length).toBe(1);
expect(notes[0].title).toBe('un');
yield switchClient(1);
disabledItems = yield BaseItem_1.default.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(1);
})));
it('should allow duplicate folder titles', (() => __awaiter(this, void 0, void 0, function* () {
yield Folder_1.default.save({ title: 'folder' });
yield switchClient(2);
let remoteF2 = yield Folder_1.default.save({ title: 'folder' });
yield synchronizerStart();
yield switchClient(1);
yield sleep(0.1);
yield synchronizerStart();
const localF2 = yield Folder_1.default.load(remoteF2.id);
expect(localF2.title == remoteF2.title).toBe(true);
// Then that folder that has been renamed locally should be set in such a way
// that synchronizing it applies the title change remotely, and that new title
// should be retrieved by client 2.
yield synchronizerStart();
yield switchClient(2);
yield sleep(0.1);
yield synchronizerStart();
remoteF2 = yield Folder_1.default.load(remoteF2.id);
expect(remoteF2.title == localF2.title).toBe(true);
})));
it('should create remote items with UTF-8 content', (() => __awaiter(this, void 0, void 0, function* () {
const folder = yield Folder_1.default.save({ title: 'Fahrräder' });
yield Note_1.default.save({ title: 'Fahrräder', body: 'Fahrräder', parent_id: folder.id });
const all = yield test_utils_synchronizer_1.allNotesFolders();
yield synchronizerStart();
yield test_utils_synchronizer_1.localNotesFoldersSameAsRemote(all, expect);
})));
it('should update remote items but not pull remote changes', (() => __awaiter(this, void 0, void 0, function* () {
const folder = yield Folder_1.default.save({ title: 'folder1' });
const note = yield Note_1.default.save({ title: 'un', parent_id: folder.id });
yield synchronizerStart();
yield switchClient(2);
yield synchronizerStart();
yield Note_1.default.save({ title: 'deux', parent_id: folder.id });
yield synchronizerStart();
yield switchClient(1);
yield Note_1.default.save({ title: 'un UPDATE', id: note.id });
yield synchronizerStart(null, { syncSteps: ['update_remote'] });
const all = yield test_utils_synchronizer_1.allNotesFolders();
expect(all.length).toBe(2);
yield switchClient(2);
yield synchronizerStart();
const note2 = yield Note_1.default.load(note.id);
expect(note2.title).toBe('un UPDATE');
})));
it('should create a new Welcome notebook on each client', (() => __awaiter(this, void 0, void 0, function* () {
// Create the Welcome items on two separate clients
yield WelcomeUtils.createWelcomeItems();
yield synchronizerStart();
yield switchClient(2);
yield WelcomeUtils.createWelcomeItems();
const beforeFolderCount = (yield Folder_1.default.all()).length;
const beforeNoteCount = (yield Note_1.default.all()).length;
expect(beforeFolderCount === 1).toBe(true);
expect(beforeNoteCount > 1).toBe(true);
yield synchronizerStart();
const afterFolderCount = (yield Folder_1.default.all()).length;
const afterNoteCount = (yield Note_1.default.all()).length;
expect(afterFolderCount).toBe(beforeFolderCount * 2);
expect(afterNoteCount).toBe(beforeNoteCount * 2);
// Changes to the Welcome items should be synced to all clients
const f1 = (yield Folder_1.default.all())[0];
yield Folder_1.default.save({ id: f1.id, title: 'Welcome MOD' });
yield synchronizerStart();
yield switchClient(1);
yield synchronizerStart();
const f1_1 = yield Folder_1.default.load(f1.id);
expect(f1_1.title).toBe('Welcome MOD');
})));
it('should not wipe out user data when syncing with an empty target', (() => __awaiter(this, void 0, void 0, function* () {
// Only these targets support the wipeOutFailSafe flag (in other words, the targets that use basicDelta)
if (!['nextcloud', 'memory', 'filesystem', 'amazon_s3'].includes(syncTargetName())) { return; }
for (let i = 0; i < 10; i++) { yield Note_1.default.save({ title: 'note' }); }
Setting_1.default.setValue('sync.wipeOutFailSafe', true);
yield synchronizerStart();
yield fileApi().clearRoot(); // oops
yield synchronizerStart();
expect((yield Note_1.default.all()).length).toBe(10); // but since the fail-safe if on, the notes have not been deleted
Setting_1.default.setValue('sync.wipeOutFailSafe', false); // Now switch it off
yield synchronizerStart();
expect((yield Note_1.default.all()).length).toBe(0); // Since the fail-safe was off, the data has been cleared
// Handle case where the sync target has been wiped out, then the user creates one note and sync.
for (let i = 0; i < 10; i++) { yield Note_1.default.save({ title: 'note' }); }
Setting_1.default.setValue('sync.wipeOutFailSafe', true);
yield synchronizerStart();
yield fileApi().clearRoot();
yield Note_1.default.save({ title: 'ma note encore' });
yield synchronizerStart();
expect((yield Note_1.default.all()).length).toBe(11);
})));
});
// # sourceMappingURL=Synchronizer.share.js.map

View File

@@ -1,39 +1,70 @@
import { afterAllCleanUp, synchronizerStart, setupDatabaseAndSynchronizer, switchClient } from './test-utils';
import Note from '@joplin/lib/models/Note';
import BaseItem from '@joplin/lib/models/BaseItem';
import shim from '@joplin/lib/shim';
import Resource from '@joplin/lib/models/Resource';
describe('Synchronizer.sharing', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should mark link resources as shared before syncing', (async () => {
let note1 = await Note.save({ title: 'note1' });
note1 = await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const resourceId1 = (await Note.linkedResourceIds(note1.body))[0];
const note2 = await Note.save({ title: 'note2' });
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
expect((await Resource.sharedResourceIds()).length).toBe(0);
await BaseItem.updateShareStatus(note1, true);
await synchronizerStart();
const sharedResourceIds = await Resource.sharedResourceIds();
expect(sharedResourceIds.length).toBe(1);
expect(sharedResourceIds[0]).toBe(resourceId1);
it('should skip', (async () => {
expect(true).toBe(true);
}));
});
// import { afterAllCleanUp, synchronizerStart, setupDatabaseAndSynchronizer, switchClient, joplinServerApi } from './test-utils';
// import Note from '@joplin/lib/models/Note';
// import BaseItem from '@joplin/lib/models/BaseItem';
// import shim from '@joplin/lib/shim';
// import Resource from '@joplin/lib/models/Resource';
// import Folder from '@joplin/lib/models/Folder';
// describe('Synchronizer.sharing', function() {
// beforeEach(async (done) => {
// await setupDatabaseAndSynchronizer(1);
// await switchClient(1);
// done();
// });
// afterAll(async () => {
// await afterAllCleanUp();
// });
// it('should mark link resources as shared before syncing', (async () => {
// let note1 = await Note.save({ title: 'note1' });
// note1 = await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
// const resourceId1 = (await Note.linkedResourceIds(note1.body))[0];
// const note2 = await Note.save({ title: 'note2' });
// await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
// expect((await Resource.sharedResourceIds()).length).toBe(0);
// await BaseItem.updateShareStatus(note1, true);
// await synchronizerStart();
// const sharedResourceIds = await Resource.sharedResourceIds();
// expect(sharedResourceIds.length).toBe(1);
// expect(sharedResourceIds[0]).toBe(resourceId1);
// }));
// it('should share items', (async () => {
// await setupDatabaseAndSynchronizer(1, { userEmail: 'user1@example.com' });
// await switchClient(1);
// const api = joplinServerApi();
// await api.exec('POST', 'api/debug', null, { action: 'createTestUsers' });
// await api.clearSession();
// const folder1 = await Folder.save({ title: 'folder1' });
// await Note.save({ title: 'note1', parent_id: folder1.id });
// await synchronizerStart();
// await setupDatabaseAndSynchronizer(2, { userEmail: 'user2@example.com' });
// await switchClient(2);
// await synchronizerStart();
// await switchClient(1);
// console.info(await Note.all());
// }));
// });

View File

@@ -0,0 +1,46 @@
import Setting from '@joplin/lib/models/Setting';
import time from '@joplin/lib/time';
describe('dateFormats', function() {
beforeEach(async (done) => {
done();
});
it('should format date according to DATE_FORMAT', (async () => {
const now = new Date('2017-01-30T12:00:00').getTime();
// DATE_FORMAT_1 = 'DD/MM/YYYY';
// DATE_FORMAT_2 = 'DD/MM/YY';
// DATE_FORMAT_3 = 'MM/DD/YYYY';
// DATE_FORMAT_4 = 'MM/DD/YY';
// DATE_FORMAT_5 = 'YYYY-MM-DD';
// DATE_FORMAT_6 = 'DD.MM.YYYY';
// DATE_FORMAT_7 = 'YYYY.MM.DD';
// DATE_FORMAT_8 = 'YYMMDD';
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_1)).toBe('30/01/2017');
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_2)).toBe('30/01/17');
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_3)).toBe('01/30/2017');
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_4)).toBe('01/30/17');
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_5)).toBe('2017-01-30');
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_6)).toBe('30.01.2017');
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_7)).toBe('2017.01.30');
expect(time.formatMsToLocal(now, Setting.DATE_FORMAT_8)).toBe('170130');
}));
it('should format time according to TIME_FORMAT', (async () => {
const now = new Date('2017-01-30T20:30:00').getTime();
// TIME_FORMAT_1 = 'HH:mm';
// TIME_FORMAT_2 = 'h:mm A';
expect(time.formatMsToLocal(now, Setting.TIME_FORMAT_1)).toBe('20:30');
expect(time.formatMsToLocal(now, Setting.TIME_FORMAT_2)).toBe('8:30 PM');
}));
});

View File

@@ -0,0 +1,69 @@
import { afterAllCleanUp, setupDatabaseAndSynchronizer, switchClient, fileApi } from './test-utils';
describe('file-api-driver', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
await fileApi().clearRoot();
done();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should create a file', (async () => {
await fileApi().put('test.txt', 'testing');
const content = await fileApi().get('test.txt');
expect(content).toBe('testing');
}));
it('should get a file info', (async () => {
await fileApi().put('test1.txt', 'testing');
await fileApi().mkdir('sub');
await fileApi().put('sub/test2.txt', 'testing');
// Note: Although the stat object includes an "isDir" property, this is
// not actually used by the synchronizer so not required by any sync
// target.
{
const stat = await fileApi().stat('test1.txt');
expect(stat.path).toBe('test1.txt');
expect(!!stat.updated_time).toBe(true);
expect(stat.isDir).toBe(false);
}
{
const stat = await fileApi().stat('sub/test2.txt');
expect(stat.path).toBe('sub/test2.txt');
expect(!!stat.updated_time).toBe(true);
expect(stat.isDir).toBe(false);
}
}));
it('should create a file in a subdirectory', (async () => {
await fileApi().mkdir('subdir');
await fileApi().put('subdir/test.txt', 'testing');
const content = await fileApi().get('subdir/test.txt');
expect(content).toBe('testing');
}));
it('should list files', (async () => {
await fileApi().mkdir('subdir');
await fileApi().put('subdir/test1.txt', 'testing1');
await fileApi().put('subdir/test2.txt', 'testing2');
const files = await fileApi().list('subdir');
expect(files.items.length).toBe(2);
expect(files.items.map((f: any) => f.path).sort()).toEqual(['test1.txt', 'test2.txt'].sort());
}));
it('should delete a file', (async () => {
await fileApi().put('test1.txt', 'testing1');
await fileApi().delete('test1.txt');
const files = await fileApi().list('');
expect(files.items.length).toBe(0);
}));
});

View File

@@ -1,135 +0,0 @@
/* eslint-disable no-unused-vars */
const uuid = require('@joplin/lib/uuid').default;
const time = require('@joplin/lib/time').default;
const { sleep, fileApi, fileContentEqual, checkThrowAsync } = require('./test-utils.js');
const shim = require('@joplin/lib/shim').default;
const fs = require('fs-extra');
const Setting = require('@joplin/lib/models/Setting').default;
const api = null;
// Adding empty test for Jest
it('will pass', () => {
expect(true).toBe(true);
});
// NOTE: These tests work with S3 and memory driver, but not
// with other targets like file system or Nextcloud.
// All this is tested in an indirect way in tests/synchronizer
// anyway.
// We keep the file here as it could be useful as a spec for
// what calls a sync target should support, but it would
// need to be fixed first.
// To test out an FileApi implementation:
// * add a SyncTarget for your driver in `test-utils.js`
// * set `syncTargetId_` to your New SyncTarget:
// `const syncTargetId_ = SyncTargetRegistry.nameToId('memory');`
// describe('fileApi', function() {
// beforeEach(async (done) => {
// api = new fileApi();
// api.clearRoot();
// done();
// });
// describe('list', function() {
// it('should return items with relative path', (async () => {
// await api.mkdir('.subfolder');
// await api.put('1', 'something on root 1');
// await api.put('.subfolder/1', 'something subfolder 1');
// await api.put('.subfolder/2', 'something subfolder 2');
// await api.put('.subfolder/3', 'something subfolder 3');
// sleep(0.8);
// const response = await api.list('.subfolder');
// const items = response.items;
// expect(items.length).toBe(3);
// expect(items[0].path).toBe('1');
// expect(items[0].updated_time).toMatch(/^\d+$/); // make sure it's using epoch timestamp
// }));
// it('should default to only files on root directory', (async () => {
// await api.mkdir('.subfolder');
// await api.put('.subfolder/1', 'something subfolder 1');
// await api.put('file1', 'something 1');
// await api.put('file2', 'something 2');
// sleep(0.6);
// const response = await api.list();
// expect(response.items.length).toBe(2);
// }));
// }); // list
// describe('delete', function() {
// it('should not error if file does not exist', (async () => {
// const hasThrown = await checkThrowAsync(async () => await api.delete('nonexistant_file'));
// expect(hasThrown).toBe(false);
// }));
// it('should delete specific file given full path', (async () => {
// await api.mkdir('deleteDir');
// await api.put('deleteDir/1', 'something 1');
// await api.put('deleteDir/2', 'something 2');
// sleep(0.4);
// await api.delete('deleteDir/1');
// let response = await api.list('deleteDir');
// expect(response.items.length).toBe(1);
// response = await api.list('deleteDir/1');
// expect(response.items.length).toBe(0);
// }));
// }); // delete
// describe('get', function() {
// it('should return null if object does not exist', (async () => {
// const response = await api.get('nonexistant_file');
// expect(response).toBe(null);
// }));
// it('should return UTF-8 encoded string by default', (async () => {
// await api.put('testnote.md', 'something 2');
// const response = await api.get('testnote.md');
// expect(response).toBe('something 2');
// }));
// it('should return a Response object and writes file to options.path, if options.target is "file"', (async () => {
// const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`;
// await api.put('testnote.md', 'something 2');
// sleep(0.2);
// const response = await api.get('testnote.md', { target: 'file', path: localFilePath });
// expect(typeof response).toBe('object');
// // expect(response.path).toBe(localFilePath);
// expect(fs.existsSync(localFilePath)).toBe(true);
// expect(fs.readFileSync(localFilePath, 'utf8')).toBe('something 2');
// }));
// }); // get
// describe('put', function() {
// it('should create file to remote path and content', (async () => {
// await api.put('putTest.md', 'I am your content');
// sleep(0.2);
// const response = await api.get('putTest.md');
// expect(response).toBe('I am your content');
// }));
// it('should upload file in options.path to remote path, if options.source is "file"', (async () => {
// const localFilePath = `${Setting.value('tempDir')}/${uuid.create()}.md`;
// fs.writeFileSync(localFilePath, 'I am the local file.');
// await api.put('testfile', 'ignore me', { source: 'file', path: localFilePath });
// sleep(0.2);
// const response = await api.get('testfile');
// expect(response).toBe('I am the local file.');
// }));
// }); // put
// });

View File

@@ -0,0 +1,362 @@
import { setupDatabaseAndSynchronizer, switchClient, createFolderTree } from './test-utils';
import Folder from '@joplin/lib/models/Folder';
import { allNotesFolders } from './test-utils-synchronizer';
import Note from '@joplin/lib/models/Note';
import shim from '@joplin/lib/shim';
import Resource from '@joplin/lib/models/Resource';
import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
import ResourceService from '@joplin/lib/services/ResourceService';
const testImagePath = `${__dirname}/../tests/support/photo.jpg`;
describe('models_Folder.sharing', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should apply the share ID to all children', (async () => {
const folder = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'note 1',
},
{
title: 'note 2',
},
{
title: 'folder 2',
children: [
{
title: 'note 3',
},
],
},
],
},
]);
await Folder.save({ id: folder.id, share_id: 'abcd1234' });
await Folder.updateAllShareIds();
const allItems = await allNotesFolders();
for (const item of allItems) {
expect(item.share_id).toBe('abcd1234');
}
}));
it('should apply the share ID to all sub-folders', (async () => {
let folder1 = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'note 1',
},
{
title: 'note 2',
},
{
title: 'folder 2',
children: [
{
title: 'note 3',
},
],
},
{
title: 'folder 3',
children: [
{
title: 'folder 4',
children: [],
},
],
},
],
},
{
title: 'folder 5',
children: [],
},
]);
await Folder.save({ id: folder1.id, share_id: 'abcd1234' });
await Folder.updateAllShareIds();
folder1 = await Folder.loadByTitle('folder 1');
const folder2 = await Folder.loadByTitle('folder 2');
const folder3 = await Folder.loadByTitle('folder 3');
const folder4 = await Folder.loadByTitle('folder 4');
const folder5 = await Folder.loadByTitle('folder 5');
expect(folder1.share_id).toBe('abcd1234');
expect(folder2.share_id).toBe('abcd1234');
expect(folder3.share_id).toBe('abcd1234');
expect(folder4.share_id).toBe('abcd1234');
expect(folder5.share_id).toBe('');
}));
it('should update the share ID when a folder is moved in or out of shared folder', (async () => {
let folder1 = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'folder 2',
children: [],
},
],
},
{
title: 'folder 3',
children: [],
},
]);
await Folder.save({ id: folder1.id, share_id: 'abcd1234' });
await Folder.updateAllShareIds();
folder1 = await Folder.loadByTitle('folder 1');
let folder2 = await Folder.loadByTitle('folder 2');
const folder3 = await Folder.loadByTitle('folder 3');
expect(folder1.share_id).toBe('abcd1234');
expect(folder2.share_id).toBe('abcd1234');
// Move the folder outside the shared folder
await Folder.save({ id: folder2.id, parent_id: folder3.id });
await Folder.updateAllShareIds();
folder2 = await Folder.loadByTitle('folder 2');
expect(folder2.share_id).toBe('');
// Move the folder inside the shared folder
{
await Folder.save({ id: folder2.id, parent_id: folder1.id });
await Folder.updateAllShareIds();
folder2 = await Folder.loadByTitle('folder 2');
expect(folder2.share_id).toBe('abcd1234');
}
}));
it('should apply the share ID to all notes', (async () => {
const folder1 = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'note 1',
},
{
title: 'note 2',
},
{
title: 'folder 2',
children: [
{
title: 'note 3',
},
],
},
],
},
{
title: 'folder 5',
children: [
{
title: 'note 4',
},
],
},
]);
await Folder.save({ id: folder1.id, share_id: 'abcd1234' });
await Folder.updateAllShareIds();
const note1: NoteEntity = await Note.loadByTitle('note 1');
const note2: NoteEntity = await Note.loadByTitle('note 2');
const note3: NoteEntity = await Note.loadByTitle('note 3');
const note4: NoteEntity = await Note.loadByTitle('note 4');
expect(note1.share_id).toBe('abcd1234');
expect(note2.share_id).toBe('abcd1234');
expect(note3.share_id).toBe('abcd1234');
expect(note4.share_id).toBe('');
}));
it('should remove the share ID when a note is moved in or out of shared folder', (async () => {
const folder1 = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'note 1',
},
],
},
{
title: 'folder 2',
children: [],
},
]);
await Folder.save({ id: folder1.id, share_id: 'abcd1234' });
await Folder.updateAllShareIds();
const note1: NoteEntity = await Note.loadByTitle('note 1');
const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
expect(note1.share_id).toBe('abcd1234');
// Move the note outside of the shared folder
await Note.save({ id: note1.id, parent_id: folder2.id });
await Folder.updateAllShareIds();
{
const note1: NoteEntity = await Note.loadByTitle('note 1');
expect(note1.share_id).toBe('');
}
// Move the note back inside the shared folder
await Note.save({ id: note1.id, parent_id: folder1.id });
await Folder.updateAllShareIds();
{
const note1: NoteEntity = await Note.loadByTitle('note 1');
expect(note1.share_id).toBe('abcd1234');
}
}));
it('should not remove the share ID of non-modified notes', (async () => {
const folder1 = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'note 1',
},
{
title: 'note 2',
},
],
},
{
title: 'folder 2',
children: [],
},
]);
await Folder.save({ id: folder1.id, share_id: 'abcd1234' });
await Folder.updateAllShareIds();
let note1: NoteEntity = await Note.loadByTitle('note 1');
let note2: NoteEntity = await Note.loadByTitle('note 2');
const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
expect(note1.share_id).toBe('abcd1234');
expect(note2.share_id).toBe('abcd1234');
await Note.save({ id: note1.id, parent_id: folder2.id });
await Folder.updateAllShareIds();
note1 = await Note.loadByTitle('note 1');
note2 = await Note.loadByTitle('note 2');
expect(note1.share_id).toBe('');
expect(note2.share_id).toBe('abcd1234');
}));
it('should apply the note share ID to its resources', async () => {
const resourceService = new ResourceService();
const folder = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'note 1',
},
{
title: 'note 2',
},
],
},
{
title: 'folder 2',
children: [],
},
]);
await Folder.save({ id: folder.id, share_id: 'abcd1234' });
await Folder.updateAllShareIds();
const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
const note1: NoteEntity = await Note.loadByTitle('note 1');
await shim.attachFileToNote(note1, testImagePath);
// We need to index the resources to populate the note_resources table
await resourceService.indexNoteResources();
const resourceId: string = (await Resource.all())[0].id;
{
const resource: ResourceEntity = await Resource.load(resourceId);
expect(resource.share_id).toBe('');
}
await Folder.updateAllShareIds();
// await NoteResource.updateResourceShareIds();
{
const resource: ResourceEntity = await Resource.load(resourceId);
expect(resource.share_id).toBe(note1.share_id);
}
await Note.save({ id: note1.id, parent_id: folder2.id });
await resourceService.indexNoteResources();
await Folder.updateAllShareIds();
// await NoteResource.updateResourceShareIds();
{
const resource: ResourceEntity = await Resource.load(resourceId);
expect(resource.share_id).toBe('');
}
});
// it('should not recursively delete when non-owner deletes a shared folder', async () => {
// const folder = await createFolderTree('', [
// {
// title: 'folder 1',
// children: [
// {
// title: 'note 1',
// },
// ],
// },
// ]);
// BaseItem.shareService_ = {
// isSharedFolderOwner: (_folderId: string) => false,
// } as any;
// await Folder.save({ id: folder.id, share_id: 'abcd1234' });
// await Folder.updateAllShareIds();
// await Folder.delete(folder.id);
// expect((await Folder.all()).length).toBe(0);
// expect((await Note.all()).length).toBe(1);
// });
});

View File

@@ -1,5 +1,5 @@
import { FolderEntity } from '@joplin/lib/services/database/types';
import { createNTestNotes, setupDatabaseAndSynchronizer, sleep, switchClient, checkThrowAsync } from './test-utils';
import { createNTestNotes, setupDatabaseAndSynchronizer, sleep, switchClient, checkThrowAsync, createFolderTree } from './test-utils';
import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
@@ -225,4 +225,61 @@ describe('models_Folder', function() {
const hasThrown = await checkThrowAsync(() => Folder.save({ id: f1.id, parent_id: f1.id }, { userSideValidation: true }));
expect(hasThrown).toBe(true);
}));
it('should get all the children of a folder', (async () => {
const folder = await createFolderTree('', [
{
title: 'folder 1',
children: [
{
title: 'note 1',
},
{
title: 'note 2',
},
{
title: 'folder 2',
children: [
{
title: 'note 3',
},
],
},
{
title: 'folder 3',
children: [],
},
],
},
{
title: 'folder 4',
children: [
{
title: 'folder 5',
children: [],
},
],
},
]);
const folder2 = await Folder.loadByTitle('folder 2');
const folder3 = await Folder.loadByTitle('folder 3');
const folder4 = await Folder.loadByTitle('folder 4');
const folder5 = await Folder.loadByTitle('folder 5');
{
const children = await Folder.allChildrenFolders(folder.id);
expect(children.map(c => c.id).sort()).toEqual([folder2.id, folder3.id].sort());
}
{
const children = await Folder.allChildrenFolders(folder4.id);
expect(children.map(c => c.id).sort()).toEqual([folder5.id].sort());
}
{
const children = await Folder.allChildrenFolders(folder5.id);
expect(children.map(c => c.id).sort()).toEqual([].sort());
}
}));
});

View File

@@ -0,0 +1,53 @@
import { setupDatabaseAndSynchronizer, db, switchClient } from './test-utils.js';
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
import SearchEngineUtils from '@joplin/lib/services/searchengine/SearchEngineUtils';
import Setting from '@joplin/lib/models/Setting';
const Note = require('@joplin/lib/models/Note').default;
let searchEngine: any = null;
describe('services_SearchEngineUtils', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
searchEngine = new SearchEngine();
searchEngine.setDb(db());
done();
});
describe('filter todos based on showCompletedTodos', function() {
it('show completed', (async () => {
const note1 = await Note.save({ title: 'abcd', body: 'body 1' });
const todo1 = await Note.save({ title: 'abcd', body: 'todo 1', is_todo: 1 });
const todo2 = await Note.save({ title: 'abcd', body: 'todo 2', is_todo: 1, todo_completed: 1590085027710 });
await Note.save({ title: 'qwer', body: 'body 2' });
await searchEngine.syncTables();
Setting.setValue('showCompletedTodos', true);
const rows = await SearchEngineUtils.notesForQuery('abcd', null, searchEngine);
expect(rows.length).toBe(3);
expect(rows.map(r=>r.id)).toContain(note1.id);
expect(rows.map(r=>r.id)).toContain(todo1.id);
expect(rows.map(r=>r.id)).toContain(todo2.id);
}));
it('hide completed', (async () => {
const note1 = await Note.save({ title: 'abcd', body: 'body 1' });
const todo1 = await Note.save({ title: 'abcd', body: 'todo 1', is_todo: 1 });
await Note.save({ title: 'qwer', body: 'body 2' });
await Note.save({ title: 'abcd', body: 'todo 2', is_todo: 1, todo_completed: 1590085027710 });
await searchEngine.syncTables();
Setting.setValue('showCompletedTodos', false);
const rows = await SearchEngineUtils.notesForQuery('abcd', null, searchEngine);
expect(rows.length).toBe(2);
expect(rows.map(r=>r.id)).toContain(note1.id);
expect(rows.map(r=>r.id)).toContain(todo1.id);
}));
});
});

View File

@@ -1,17 +0,0 @@
import { ExportModule, ImportModule } from './types';
/**
* Provides a way to create modules to import external data into Joplin or to export notes into any arbitrary format.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export)
*
* To implement an import or export module, you would simply define an object with various event handlers that are called
* by the application during the import/export process.
*
* See the documentation of the [[ExportModule]] and [[ImportModule]] for more information.
*
* You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/api/references/rest_api/
*/
export default class JoplinInterop {
registerExportModule(module: ExportModule): Promise<void>;
registerImportModule(module: ImportModule): Promise<void>;
}

View File

@@ -53,6 +53,7 @@ import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
const WebDavApi = require('@joplin/lib/WebDavApi');
const DropboxApi = require('@joplin/lib/DropboxApi');
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { FolderEntity } from '@joplin/lib/services/database/types';
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3');
@@ -346,6 +347,29 @@ async function setupDatabase(id: number = null, options: any = null) {
await loadKeychainServiceAndSettings(options.keychainEnabled ? KeychainServiceDriver : KeychainServiceDriverDummy);
}
export async function createFolderTree(parentId: string, tree: any[], num: number = 0): Promise<FolderEntity> {
let rootFolder: FolderEntity = null;
for (const item of tree) {
const isFolder = !!item.children;
num++;
const data = { ...item };
delete data.children;
if (isFolder) {
const folder = await Folder.save({ title: `Folder ${num}`, parent_id: parentId, ...data });
if (!rootFolder) rootFolder = folder;
if (item.children.length) await createFolderTree(folder.id, item.children, num);
} else {
await Note.save({ title: `Note ${num}`, parent_id: parentId, ...data });
}
}
return rootFolder;
}
function exportDir(id: number = null) {
if (id === null) id = currentClient_;
return `${dataDir}/export`;
@@ -385,7 +409,7 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) {
if (!synchronizers_[id]) {
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
const syncTarget = new SyncTargetClass(db(id));
await initFileApi(suiteName_);
await initFileApi();
syncTarget.setFileApi(fileApi());
syncTarget.setLogger(logger);
synchronizers_[id] = await syncTarget.synchronizer();
@@ -484,7 +508,13 @@ async function loadEncryptionMasterKey(id: number = null, useExisting = false) {
return masterKey;
}
async function initFileApi(suiteName: string) {
function mustRunInBand() {
if (!process.argv.includes('--runInBand')) {
throw new Error('Tests must be run sequentially for this sync target, with the --runInBand arg. eg `npm test -- --runInBand`');
}
}
async function initFileApi() {
if (fileApis_[syncTargetId_]) return;
let fileApi = null;
@@ -524,9 +554,7 @@ async function initFileApi(suiteName: string) {
// OneDrive app directory, and it's not clear how to get that
// working.
if (!process.argv.includes('--runInBand')) {
throw new Error('OneDrive tests must be run sequentially, with the --runInBand arg. eg `npm test -- --runInBand`');
}
mustRunInBand();
const { parameters, setEnvOverride } = require('@joplin/lib/parameters.js');
Setting.setConstant('env', 'dev');
@@ -550,6 +578,8 @@ async function initFileApi(suiteName: string) {
const api = new S3({ accessKeyId: amazonS3Creds.accessKeyId, secretAccessKey: amazonS3Creds.secretAccessKey, s3UseArnRegion: true });
fileApi = new FileApi('', new FileApiDriverAmazonS3(api, amazonS3Creds.bucket));
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('joplinServer')) {
mustRunInBand();
// Note that to test the API in parallel mode, you need to use Postgres
// as database, as the SQLite database is not reliable when being
// read/write from multiple processes at the same time.
@@ -558,7 +588,8 @@ async function initFileApi(suiteName: string) {
username: () => 'admin@localhost',
password: () => 'admin',
});
fileApi = new FileApi(`Apps/Joplin-${suiteName}`, new FileApiDriverJoplinServer(api));
fileApi = new FileApi('', new FileApiDriverJoplinServer(api));
}
fileApi.setLogger(logger);

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "1.8.0",
"version": "2.0.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",

View File

@@ -7,4 +7,5 @@ gui/note-viewer/pluginAssets/
pluginAssets/
gui/note-viewer/fonts/
gui/note-viewer/lib.js
gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
runForSharingCommands-*

View File

@@ -13,6 +13,7 @@ import Logger, { TargetType } from '@joplin/lib/Logger';
import Setting from '@joplin/lib/models/Setting';
import actionApi from '@joplin/lib/services/rest/actionApi.desktop';
import BaseApplication from '@joplin/lib/BaseApplication';
import DebugService from '@joplin/lib/debug/DebugService';
import { _, setLocale } from '@joplin/lib/locale';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import SpellCheckerServiceDriverNative from './services/spellChecker/SpellCheckerServiceDriverNative';
@@ -58,6 +59,7 @@ const commands = [
require('./gui/MainScreen/commands/openTag'),
require('./gui/MainScreen/commands/print'),
require('./gui/MainScreen/commands/renameFolder'),
require('./gui/MainScreen/commands/showShareFolderDialog'),
require('./gui/MainScreen/commands/renameTag'),
require('./gui/MainScreen/commands/search'),
require('./gui/MainScreen/commands/selectTemplate'),
@@ -100,6 +102,8 @@ const globalCommands = [
];
import editorCommandDeclarations from './gui/NoteEditor/commands/editorCommandDeclarations';
import ShareService from '@joplin/lib/services/share/ShareService';
import checkForUpdates from './checkForUpdates';
const pluginClasses = [
require('./plugins/GotoAnything').default,
@@ -164,10 +168,6 @@ class Application extends BaseApplication {
return true;
}
checkForUpdateLoggerPath() {
return `${Setting.value('profileDir')}/log-autoupdater.txt`;
}
reducer(state: AppState = appDefaultState, action: any) {
let newState = state;
@@ -708,7 +708,7 @@ class Application extends BaseApplication {
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
bridge().checkForUpdates(true, bridge().window(), this.checkForUpdateLoggerPath(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
};
@@ -730,6 +730,8 @@ class Application extends BaseApplication {
bridge().window().show();
}
void ShareService.instance().maintenance();
ResourceService.runInBackground();
if (Setting.value('env') === 'dev') {
@@ -764,15 +766,16 @@ class Application extends BaseApplication {
RevisionService.instance().runInBackground();
// Make it available to the console window - useful to call revisionService.collectRevisions()
(window as any).joplin = () => {
return {
if (Setting.value('env') === 'dev') {
(window as any).joplin = {
revisionService: RevisionService.instance(),
migrationService: MigrationService.instance(),
decryptionWorker: DecryptionWorker.instance(),
commandService: CommandService.instance(),
bridge: bridge(),
debug: new DebugService(reg.db()),
};
};
}
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);

View File

@@ -1,6 +1,5 @@
import ElectronAppWrapper from './ElectronAppWrapper';
import shim from '@joplin/lib/shim';
import { _, setLocale } from '@joplin/lib/locale';
const { dirname, toSystemSlashes } = require('@joplin/lib/path-utils');
const { BrowserWindow, nativeTheme } = require('electron');
@@ -174,11 +173,6 @@ export class Bridge {
return require('electron').shell.openPath(fullPath);
}
checkForUpdates(inBackground: boolean, window: any, logFilePath: string, options: any) {
const { checkForUpdates } = require('./checkForUpdates.js');
checkForUpdates(inBackground, window, logFilePath, options);
}
buildDir() {
return this.electronApp().buildDir();
}

View File

@@ -1,44 +1,42 @@
const { dialog } = require('electron');
const shim = require('@joplin/lib/shim').default;
const Logger = require('@joplin/lib/Logger').default;
const { _ } = require('@joplin/lib/locale');
const fetch = require('node-fetch');
import shim from '@joplin/lib/shim';
import Logger from '@joplin/lib/Logger';
import { _ } from '@joplin/lib/locale';
import bridge from './services/bridge';
import KvStore from '@joplin/lib/services/KvStore';
const { fileExtension } = require('@joplin/lib/path-utils');
const ArrayUtils = require('@joplin/lib/ArrayUtils');
const packageInfo = require('./packageInfo.js');
const compareVersions = require('compare-versions');
let autoUpdateLogger_ = new Logger();
const logger = Logger.create('checkForUpdates');
let checkInBackground_ = false;
let isCheckingForUpdate_ = false;
let parentWindow_ = null;
function showErrorMessageBox(message) {
return dialog.showMessageBox(parentWindow_, {
type: 'error',
message: message,
});
interface CheckForUpdateOptions {
includePreReleases?: boolean;
}
function onCheckStarted() {
autoUpdateLogger_.info('checkForUpdates: Starting...');
logger.info('Starting...');
isCheckingForUpdate_ = true;
}
function onCheckEnded() {
autoUpdateLogger_.info('checkForUpdates: Done.');
logger.info('Done.');
isCheckingForUpdate_ = false;
}
function getMajorMinorTagName(tagName) {
function getMajorMinorTagName(tagName: string) {
const s = tagName.split('.');
s.pop();
return s.join('.');
}
async function fetchLatestRelease(options) {
async function fetchLatestRelease(options: CheckForUpdateOptions) {
options = Object.assign({}, { includePreReleases: false }, options);
const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases');
const response = await shim.fetch('https://api.github.com/repos/laurent22/joplin/releases');
if (!response.ok) {
const responseText = await response.text();
@@ -104,7 +102,7 @@ async function fetchLatestRelease(options) {
}
}
function cleanUpReleaseNotes(releaseNotes) {
function cleanUpReleaseNotes(releaseNotes: string[]) {
const lines = releaseNotes.join('\n\n* * *\n\n').split('\n');
const output = [];
for (const line of lines) {
@@ -129,7 +127,7 @@ async function fetchLatestRelease(options) {
};
}
function truncateText(text, length) {
function truncateText(text: string, length: number) {
let truncated = text.substring(0, length);
const lastNewLine = truncated.lastIndexOf('\n');
// Cut off at a line break unless we'd be cutting off half the text
@@ -141,66 +139,80 @@ function truncateText(text, length) {
return truncated;
}
function checkForUpdates(inBackground, window, logFilePath, options) {
async function getSkippedVersions(): Promise<string[]> {
const r = await KvStore.instance().value<string>('updateCheck::skippedVersions');
return r ? JSON.parse(r) : [];
}
async function isSkippedVersion(v: string): Promise<boolean> {
const versions = await getSkippedVersions();
return versions.includes(v);
}
async function addSkippedVersion(s: string) {
let versions = await getSkippedVersions();
versions.push(s);
versions = ArrayUtils.unique(versions);
await KvStore.instance().setValue('updateCheck::skippedVersions', JSON.stringify(versions));
}
export default async function checkForUpdates(inBackground: boolean, parentWindow: any, options: CheckForUpdateOptions) {
if (isCheckingForUpdate_) {
autoUpdateLogger_.info('checkForUpdates: Skipping check because it is already running');
logger.info('Skipping check because it is already running');
return;
}
parentWindow_ = window;
onCheckStarted();
if (logFilePath && !autoUpdateLogger_.targets().length) {
autoUpdateLogger_ = new Logger();
autoUpdateLogger_.addTarget('file', { path: logFilePath });
autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
autoUpdateLogger_.info('checkForUpdates: Initializing...');
}
checkInBackground_ = inBackground;
autoUpdateLogger_.info(`checkForUpdates: Checking with options ${JSON.stringify(options)}`);
logger.info(`Checking with options ${JSON.stringify(options)}`);
fetchLatestRelease(options).then(async (release) => {
autoUpdateLogger_.info(`Current version: ${packageInfo.version}`);
autoUpdateLogger_.info(`Latest version: ${release.version}`);
autoUpdateLogger_.info('Is Pre-release:', release.prerelease);
try {
const release = await fetchLatestRelease(options);
logger.info(`Current version: ${packageInfo.version}`);
logger.info(`Latest version: ${release.version}`);
logger.info('Is Pre-release:', release.prerelease);
if (compareVersions(release.version, packageInfo.version) <= 0) {
if (!checkInBackground_) {
await dialog.showMessageBox({
type: 'info',
message: _('Current version is up-to-date.'),
buttons: [_('OK')],
});
await bridge().showMessageBox(_('Current version is up-to-date.'));
}
} else {
const fullReleaseNotes = release.notes.trim() ? `\n\n${release.notes.trim()}` : '';
const MAX_RELEASE_NOTES_LENGTH = 1000;
const truncateReleaseNotes = fullReleaseNotes.length > MAX_RELEASE_NOTES_LENGTH;
const releaseNotes = truncateReleaseNotes ? truncateText(fullReleaseNotes, MAX_RELEASE_NOTES_LENGTH) : fullReleaseNotes;
const shouldSkip = checkInBackground_ && await isSkippedVersion(release.version);
const newVersionString = release.prerelease ? _('%s (pre-release)', release.version) : release.version;
if (shouldSkip) {
logger.info('Not displaying notification because version has been skipped');
} else {
const fullReleaseNotes = release.notes.trim() ? `\n\n${release.notes.trim()}` : '';
const MAX_RELEASE_NOTES_LENGTH = 1000;
const truncateReleaseNotes = fullReleaseNotes.length > MAX_RELEASE_NOTES_LENGTH;
const releaseNotes = truncateReleaseNotes ? truncateText(fullReleaseNotes, MAX_RELEASE_NOTES_LENGTH) : fullReleaseNotes;
const result = await dialog.showMessageBox(parentWindow_, {
type: 'info',
message: `${_('An update is available, do you want to download it now?')}`,
detail: `${_('Your version: %s', packageInfo.version)}\n${_('New version: %s', newVersionString)}${releaseNotes}`,
buttons: [_('Download'), _('Cancel'), _('Full changelog')],
cancelId: 1,
});
const newVersionString = release.prerelease ? _('%s (pre-release)', release.version) : release.version;
const buttonIndex = result.response;
if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
if (buttonIndex === 2) require('electron').shell.openExternal('https://joplinapp.org/changelog/');
const buttonIndex = await bridge().showMessageBox(parentWindow, {
type: 'info',
message: `${_('An update is available, do you want to download it now?')}`,
detail: `${_('Your version: %s', packageInfo.version)}\n${_('New version: %s', newVersionString)}${releaseNotes}`,
buttons: [_('Download'), _('Skip this version'), _('Full changelog'), _('Cancel')],
cancelId: 3,
});
if (buttonIndex === 0) {
bridge().openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
} else if (buttonIndex === 1) {
await addSkippedVersion(release.version);
} else if (buttonIndex === 2) {
bridge().openExternal('https://joplinapp.org/changelog/');
}
}
}
}).catch(error => {
autoUpdateLogger_.error(error);
if (!checkInBackground_) showErrorMessageBox(error.message);
}).then(() => {
} catch (error) {
logger.error(error);
if (!checkInBackground_) await bridge().showErrorMessageBox(error.message);
} finally {
onCheckEnded();
});
}
}
module.exports.checkForUpdates = checkForUpdates;

View File

@@ -0,0 +1,40 @@
import styled from 'styled-components';
const DialogModalLayer = styled.div`
z-index: 9999;
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.6);
align-items: flex-start;
justify-content: center;
overflow: hidden;
`;
const DialogRoot = styled.div`
background-color: ${props => props.theme.backgroundColor};
padding: 16px;
box-shadow: 6px 6px 20px rgba(0,0,0,0.5);
margin-top: 20px;
min-height: fit-content;
display: flex;
flex-direction: column;
width: 50%;
`;
interface Props {
renderContent: Function;
}
export default function Dialog(props: Props) {
return (
<DialogModalLayer>
<DialogRoot>
{props.renderContent()}
</DialogRoot>
</DialogModalLayer>
);
}

View File

@@ -2,7 +2,26 @@ const React = require('react');
const { _ } = require('@joplin/lib/locale');
const { themeStyle } = require('@joplin/lib/theme');
function DialogButtonRow(props) {
export interface ButtonSpec {
name: string;
label: string;
}
export interface ClickEvent {
buttonName: string;
}
interface Props {
themeId: number;
onClick?: (event: ClickEvent)=> void;
okButtonShow?: boolean;
cancelButtonShow?: boolean;
cancelButtonLabel?: string;
okButtonRef?: any;
customButtons?: ButtonSpec[];
}
export default function DialogButtonRow(props: Props) {
const theme = themeStyle(props.themeId);
const okButton_click = () => {
@@ -13,7 +32,11 @@ function DialogButtonRow(props) {
if (props.onClick) props.onClick({ buttonName: 'cancel' });
};
const onKeyDown = (event) => {
const customButton_click = (event: ClickEvent) => {
if (props.onClick) props.onClick(event);
};
const onKeyDown = (event: any) => {
if (event.keyCode === 13) {
okButton_click();
} else if (event.keyCode === 27) {
@@ -23,6 +46,16 @@ function DialogButtonRow(props) {
const buttonComps = [];
if (props.customButtons) {
for (const b of props.customButtons) {
buttonComps.push(
<button key={b.name} style={theme.buttonStyle} onClick={() => customButton_click({ buttonName: b.name })} onKeyDown={onKeyDown}>
{b.label}
</button>
);
}
}
if (props.okButtonShow !== false) {
buttonComps.push(
<button key="ok" style={theme.buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
@@ -41,5 +74,3 @@ function DialogButtonRow(props) {
return <div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>;
}
module.exports = DialogButtonRow;

View File

@@ -0,0 +1,21 @@
import styled from 'styled-components';
const Root = styled.div`
font-family: ${props => props.theme.fontFamily};
font-size: ${props => props.theme.fontSize * 1.5};
line-height: 1.6em;
color: ${props => props.theme.color};
font-weight: bold;
margin-bottom: 1.2em;
`;
interface Props {
title: string;
}
export default function DialogTitle(props: Props) {
return (
<Root>{props.title}</Root>
);
}

View File

@@ -29,12 +29,16 @@ import { themeStyle } from '@joplin/lib/theme';
import validateLayout from '../ResizableLayout/utils/validateLayout';
import iterateItems from '../ResizableLayout/utils/iterateItems';
import removeItem from '../ResizableLayout/utils/removeItem';
import EncryptionService from '@joplin/lib/services/EncryptionService';
import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog';
import { ShareInvitation } from '@joplin/lib/services/share/reducer';
import ShareService from '@joplin/lib/services/share/ShareService';
import { reg } from '@joplin/lib/registry';
const { connect } = require('react-redux');
const { PromptDialog } = require('../PromptDialog.min.js');
const NotePropertiesDialog = require('../NotePropertiesDialog.min.js');
const PluginManager = require('@joplin/lib/services/PluginManager');
import EncryptionService from '@joplin/lib/services/EncryptionService';
const ipcRenderer = require('electron').ipcRenderer;
interface LayerModalState {
@@ -63,15 +67,22 @@ interface Props {
settingEditorCodeView: boolean;
pluginsLegacy: any;
startupPluginsLoaded: boolean;
shareInvitations: ShareInvitation[];
isSafeMode: boolean;
}
interface ShareFolderDialogOptions {
folderId: string;
visible: boolean;
}
interface State {
promptOptions: any;
modalLayer: LayerModalState;
notePropertiesDialogOptions: any;
noteContentPropertiesDialogOptions: any;
shareNoteDialogOptions: any;
shareFolderDialogOptions: ShareFolderDialogOptions;
}
const StyledUserWebviewDialogContainer = styled.div`
@@ -105,6 +116,7 @@ const commands = [
require('./commands/newTodo'),
require('./commands/print'),
require('./commands/renameFolder'),
require('./commands/showShareFolderDialog'),
require('./commands/renameTag'),
require('./commands/search'),
require('./commands/selectTemplate'),
@@ -144,6 +156,10 @@ class MainScreenComponent extends React.Component<Props, State> {
notePropertiesDialogOptions: {},
noteContentPropertiesDialogOptions: {},
shareNoteDialogOptions: {},
shareFolderDialogOptions: {
visible: false,
folderId: '',
},
};
this.updateMainLayout(this.buildLayout(props.plugins));
@@ -155,6 +171,7 @@ class MainScreenComponent extends React.Component<Props, State> {
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
this.shareFolderDialog_close = this.shareFolderDialog_close.bind(this);
this.resizableLayout_resize = this.resizableLayout_resize.bind(this);
this.resizableLayout_renderItem = this.resizableLayout_renderItem.bind(this);
this.resizableLayout_moveButtonClick = this.resizableLayout_moveButtonClick.bind(this);
@@ -204,6 +221,10 @@ class MainScreenComponent extends React.Component<Props, State> {
return newLayout !== layout ? validateLayout(newLayout) : layout;
}
private showShareInvitationNotification(props: Props): boolean {
return !!props.shareInvitations.find(i => i.status === 0);
}
private buildLayout(plugins: PluginStates): LayoutItem {
const rootLayoutSize = this.rootLayoutSize();
@@ -268,10 +289,14 @@ class MainScreenComponent extends React.Component<Props, State> {
this.setState({ noteContentPropertiesDialogOptions: {} });
}
shareNoteDialog_close() {
private shareNoteDialog_close() {
this.setState({ shareNoteDialogOptions: {} });
}
private shareFolderDialog_close() {
this.setState({ shareFolderDialogOptions: { visible: false, folderId: '' } });
}
updateMainLayout(layout: LayoutItem) {
this.props.dispatch({
type: 'MAIN_LAYOUT_SET',
@@ -321,6 +346,13 @@ class MainScreenComponent extends React.Component<Props, State> {
});
}
if (this.state.shareFolderDialogOptions !== prevState.shareFolderDialogOptions) {
this.props.dispatch({
type: this.state.shareFolderDialogOptions && this.state.shareFolderDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE',
name: 'shareFolder',
});
}
if (this.props.mainLayout !== prevProps.mainLayout) {
const toSave = saveLayout(this.props.mainLayout);
Setting.setValue('ui.layout', toSave);
@@ -496,6 +528,12 @@ class MainScreenComponent extends React.Component<Props, State> {
bridge().restart();
};
const onInvitationRespond = async (shareUserId: string, accept: boolean) => {
await ShareService.instance().respondInvitation(shareUserId, accept);
await ShareService.instance().refreshShareInvitations();
void reg.scheduleSync(1000);
};
let msg = null;
if (this.props.isSafeMode) {
@@ -516,15 +554,6 @@ class MainScreenComponent extends React.Component<Props, State> {
</a>
</span>
);
} else if (this.props.hasDisabledSyncItems) {
msg = (
<span>
{_('Some items cannot be synchronised.')}{' '}
<a href="#" onClick={() => onViewStatusScreen()}>
{_('View them now')}
</a>
</span>
);
} else if (this.props.hasDisabledEncryptionItems) {
msg = (
<span>
@@ -534,15 +563,6 @@ class MainScreenComponent extends React.Component<Props, State> {
</a>
</span>
);
} else if (this.props.showMissingMasterKeyMessage) {
msg = (
<span>
{_('One or more master keys need a password.')}{' '}
<a href="#" onClick={() => onViewEncryptionConfigScreen()}>
{_('Set the password')}
</a>
</span>
);
} else if (this.props.showNeedUpgradingMasterKeyMessage) {
msg = (
<span>
@@ -561,6 +581,40 @@ class MainScreenComponent extends React.Component<Props, State> {
</a>
</span>
);
} else if (this.showShareInvitationNotification(this.props)) {
const invitation = this.props.shareInvitations[0];
const sharer = invitation.share.user;
msg = (
<span>
{_('%s (%s) would like to share a notebook with you.', sharer.full_name, sharer.email)}{' '}
<a href="#" onClick={() => onInvitationRespond(invitation.id, true)}>
{_('Accept')}
</a>
{' / '}
<a href="#" onClick={() => onInvitationRespond(invitation.id,true)}>
{_('Reject')}
</a>
</span>
);
} else if (this.props.hasDisabledSyncItems) {
msg = (
<span>
{_('Some items cannot be synchronised.')}{' '}
<a href="#" onClick={() => onViewStatusScreen()}>
{_('View them now')}
</a>
</span>
);
} else if (this.props.showMissingMasterKeyMessage) {
msg = (
<span>
{_('One or more master keys need a password.')}{' '}
<a href="#" onClick={() => onViewEncryptionConfigScreen()}>
{_('Set the password')}
</a>
</span>
);
}
return (
@@ -572,7 +626,7 @@ class MainScreenComponent extends React.Component<Props, State> {
messageBoxVisible(props: Props = null) {
if (!props) props = this.props;
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode;
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props);
}
registerCommands() {
@@ -739,6 +793,7 @@ class MainScreenComponent extends React.Component<Props, State> {
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
const shareFolderDialogOptions = this.state.shareFolderDialogOptions;
const layoutComp = this.props.mainLayout ? (
<ResizableLayout
@@ -759,6 +814,7 @@ class MainScreenComponent extends React.Component<Props, State> {
{noteContentPropertiesDialogOptions.visible && <NoteContentPropertiesDialog markupLanguage={noteContentPropertiesDialogOptions.markupLanguage} themeId={this.props.themeId} onClose={this.noteContentPropertiesDialog_close} text={noteContentPropertiesDialogOptions.text}/>}
{notePropertiesDialogOptions.visible && <NotePropertiesDialog themeId={this.props.themeId} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
{shareNoteDialogOptions.visible && <ShareNoteDialog themeId={this.props.themeId} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
{shareFolderDialogOptions.visible && <ShareFolderDialog themeId={this.props.themeId} folderId={shareFolderDialogOptions.folderId} onClose={this.shareFolderDialog_close} />}
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} themeId={this.props.themeId} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
@@ -794,6 +850,7 @@ const mapStateToProps = (state: AppState) => {
layoutMoveMode: state.layoutMoveMode,
mainLayout: state.mainLayout,
startupPluginsLoaded: state.startupPluginsLoaded,
shareInvitations: state.shareService.shareInvitations,
isSafeMode: state.settings.isSafeMode,
};
};

View File

@@ -0,0 +1,23 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'showShareFolderDialog',
label: () => _('Share notebook...'),
};
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async (context: CommandContext, folderId: string = null) => {
folderId = folderId || context.state.selectedFolderId;
comp.setState({
shareFolderDialogOptions: {
folderId,
visible: true,
},
});
},
enabledCondition: 'folderIsShareRootAndOwnedByUser || !folderIsShared',
};
};

View File

@@ -17,6 +17,7 @@ import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerS
import menuCommandNames from './menuCommandNames';
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
import bridge from '../services/bridge';
import checkForUpdates from '../checkForUpdates';
const { connect } = require('react-redux');
import { reg } from '@joplin/lib/registry';
@@ -430,7 +431,7 @@ function useMenu(props: Props) {
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.language'], props['spellChecker.enabled']));
function _checkForUpdates() {
bridge().checkForUpdates(false, bridge().window(), `${Setting.value('profileDir')}/log-autoupdater.txt`, { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
function _showAbout() {

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import { _ } from '@joplin/lib/locale';
import DialogButtonRow from './DialogButtonRow';
const { themeStyle } = require('@joplin/lib/theme');
const DialogButtonRow = require('./DialogButtonRow.min');
const Countable = require('countable');
import markupLanguageUtils from '../utils/markupLanguageUtils';

View File

@@ -163,7 +163,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll });
usePluginServiceRegistration(ref);
useContextMenu(editor, props.plugins);
useContextMenu(editor, props.plugins, props.dispatch);
const dispatchDidUpdate = (editor: any) => {
if (dispatchDidUpdateIID_) shim.clearTimeout(dispatchDidUpdateIID_);

View File

@@ -39,11 +39,11 @@ interface ContextMenuActionOptions {
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
export default function(editor: any, plugins: PluginStates) {
export default function(editor: any, plugins: PluginStates, dispatch: Function) {
useEffect(() => {
if (!editor) return () => {};
const contextMenuItems = menuItems();
const contextMenuItems = menuItems(dispatch);
function onContextMenu(_event: any, params: any) {
const element = contextMenuElement(editor, params.x, params.y);
@@ -110,5 +110,5 @@ export default function(editor: any, plugins: PluginStates) {
bridge().window().webContents.off('context-menu', onContextMenu);
}
};
}, [editor, plugins]);
}, [editor, plugins, dispatch]);
}

View File

@@ -6,7 +6,10 @@ const bridge = require('electron').remote.require('./bridge').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
import Resource from '@joplin/lib/models/Resource';
import BaseItem from '@joplin/lib/models/BaseItem';
import BaseModel from '@joplin/lib/BaseModel';
import { processPastedHtml } from './resourceHandling';
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
const fs = require('fs-extra');
const { clipboard } = require('electron');
const { toSystemSlashes } = require('@joplin/lib/path-utils');
@@ -53,17 +56,50 @@ function handleCopyToClipboard(options: ContextMenuOptions) {
}
}
export function menuItems(): ContextMenuItems {
export async function openItemById(itemId: string, dispatch: Function, hash: string = '') {
const item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error(`No item with ID ${itemId}`);
if (item.type_ === BaseModel.TYPE_RESOURCE) {
const resource = item as ResourceEntity;
const localState = await Resource.localState(resource);
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE || !!resource.encryption_blob_encrypted) {
if (localState.fetch_status === Resource.FETCH_STATUS_ERROR) {
bridge().showErrorMessageBox(`${_('There was an error downloading this attachment:')}\n\n${localState.fetch_error}`);
} else {
bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet'));
}
return;
}
try {
await ResourceEditWatcher.instance().openAndWatch(resource.id);
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(error.message);
}
} else if (item.type_ === BaseModel.TYPE_NOTE) {
const note = item as NoteEntity;
dispatch({
type: 'FOLDER_AND_NOTE_SELECT',
folderId: note.parent_id,
noteId: note.id,
hash,
});
} else {
throw new Error(`Unsupported item type: ${item.type_}`);
}
}
export function menuItems(dispatch: Function): ContextMenuItems {
return {
open: {
label: _('Open...'),
onAction: async (options: ContextMenuOptions) => {
try {
await ResourceEditWatcher.instance().openAndWatch(options.resourceId);
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(error.message);
}
await openItemById(options.resourceId, dispatch);
},
isActive: (itemType: ContextMenuItemType) => itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
},
@@ -134,10 +170,10 @@ export function menuItems(): ContextMenuItems {
};
}
export default async function contextMenu(options: ContextMenuOptions) {
export default async function contextMenu(options: ContextMenuOptions, dispatch: Function) {
const menu = new Menu();
const items = menuItems();
const items = menuItems(dispatch);
if (!('readyOnly' in options)) options.isReadOnly = true;

View File

@@ -1,13 +1,9 @@
import { useCallback } from 'react';
import { FormNote } from './types';
import contextMenu from './contextMenu';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import contextMenu, { openItemById } from './contextMenu';
import { _ } from '@joplin/lib/locale';
import CommandService from '@joplin/lib/services/CommandService';
import PostMessageService from '@joplin/lib/services/PostMessageService';
import BaseItem from '@joplin/lib/models/BaseItem';
import BaseModel from '@joplin/lib/BaseModel';
import Resource from '@joplin/lib/models/Resource';
const bridge = require('electron').remote.require('./bridge').default;
const { urlDecode } = require('@joplin/lib/string-utils');
const urlUtils = require('@joplin/lib/urlUtils');
@@ -46,43 +42,13 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
linkToCopy: arg0.linkToCopy || null,
htmlToCopy: '',
insertContent: () => { console.warn('insertContent() not implemented'); },
});
}, dispatch);
menu.popup(bridge().window());
} else if (msg.indexOf('joplin://') === 0) {
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
const itemId = resourceUrlInfo.itemId;
const item = await BaseItem.loadItemById(itemId);
const { itemId, hash } = urlUtils.parseResourceUrl(msg);
await openItemById(itemId, dispatch, hash);
if (!item) throw new Error(`No item with ID ${itemId}`);
if (item.type_ === BaseModel.TYPE_RESOURCE) {
const localState = await Resource.localState(item);
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE || !!item.encryption_blob_encrypted) {
if (localState.fetch_status === Resource.FETCH_STATUS_ERROR) {
bridge().showErrorMessageBox(`${_('There was an error downloading this attachment:')}\n\n${localState.fetch_error}`);
} else {
bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet'));
}
return;
}
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',
folderId: item.parent_id,
noteId: item.id,
hash: resourceUrlInfo.hash,
});
} else {
throw new Error(`Unsupported item type: ${item.type_}`);
}
} else if (urlUtils.urlProtocol(msg)) {
if (msg.indexOf('file://') === 0) {
// When using the file:// protocol, openPath doesn't work (does nothing) with URL-encoded paths

View File

@@ -2,7 +2,7 @@ const React = require('react');
const { _ } = require('@joplin/lib/locale');
const { themeStyle } = require('@joplin/lib/theme');
const time = require('@joplin/lib/time').default;
const DialogButtonRow = require('./DialogButtonRow.min');
const DialogButtonRow = require('./DialogButtonRow').default;
const Datetime = require('react-datetime');
const Note = require('@joplin/lib/models/Note').default;
const formatcoords = require('formatcoords');

View File

@@ -0,0 +1,336 @@
import Dialog from '../Dialog';
import DialogButtonRow, { ClickEvent, ButtonSpec } from '../DialogButtonRow';
import DialogTitle from '../DialogTitle';
import { _ } from '@joplin/lib/locale';
import { useEffect, useState } from 'react';
import { FolderEntity } from '@joplin/lib/services/database/types';
import Folder from '@joplin/lib/models/Folder';
import ShareService from '@joplin/lib/services/share/ShareService';
import styled from 'styled-components';
import StyledFormLabel from '../style/StyledFormLabel';
import StyledInput from '../style/StyledInput';
import Button from '../Button/Button';
import Logger from '@joplin/lib/Logger';
import StyledMessage from '../style/StyledMessage';
import { ShareUserStatus, StateShare, StateShareUser } from '@joplin/lib/services/share/reducer';
import { State } from '@joplin/lib/reducer';
import { connect } from 'react-redux';
import { reg } from '@joplin/lib/registry';
const logger = Logger.create('ShareFolderDialog');
const StyledFolder = styled.div`
border: 1px solid ${(props) => props.theme.dividerColor};
padding: 0.5em;
margin-bottom: 1em;
display: flex;
align-items: center;
`;
const StyledRecipientControls = styled.div`
display: flex;
flex-direction: row;
`;
const StyledRecipientInput = styled(StyledInput)`
width: 100%;
margin-right: 10px;
`;
const StyledAddRecipient = styled.div`
margin-bottom: 1em;
`;
const StyledRecipient = styled(StyledMessage)`
display: flex;
flex-direction: row;
padding: .6em 1em;
background-color: ${props => props.index % 2 === 0 ? props.theme.backgroundColor : props.theme.oddBackgroundColor};
align-items: center;
`;
const StyledRecipientName = styled.div`
display: flex;
flex: 1;
`;
const StyledRecipientStatusIcon = styled.i`
margin-right: .6em;
`;
const StyledRecipients = styled.div`
`;
const StyledRecipientList = styled.div`
border: 1px solid ${(props: any) => props.theme.dividerColor};
border-radius: 3px;
height: 300px;
overflow-x: hidden;
overflow-y: scroll;
`;
const StyledError = styled(StyledMessage)`
word-break: break-all;
margin-bottom: 1em;
`;
const StyledShareState = styled(StyledMessage)`
word-break: break-all;
margin-bottom: 1em;
`;
const StyledIcon = styled.i`
margin-right: 8px;
`;
interface Props {
themeId: number;
folderId: string;
onClose(): void;
shares: StateShare[];
shareUsers: Record<string, StateShareUser[]>;
}
interface RecipientDeleteEvent {
shareUserId: string;
}
interface AsyncEffectEvent {
cancelled: boolean;
}
function useAsyncEffect(effect: Function, dependencies: any[]) {
useEffect(() => {
const event = { cancelled: false };
effect(event);
return () => {
event.cancelled = true;
};
}, dependencies);
}
enum ShareState {
Idle = 0,
Synchronizing = 1,
Creating = 2,
}
function ShareFolderDialog(props: Props) {
const [folder, setFolder] = useState<FolderEntity>(null);
const [recipientEmail, setRecipientEmail] = useState<string>('');
const [latestError, setLatestError] = useState<Error>(null);
const [share, setShare] = useState<StateShare>(null);
const [shareUsers, setShareUsers] = useState<StateShareUser[]>([]);
const [shareState, setShareState] = useState<ShareState>(ShareState.Idle);
const [customButtons, setCustomButtons] = useState<ButtonSpec[]>([]);
async function synchronize(event: AsyncEffectEvent = null) {
setShareState(ShareState.Synchronizing);
await reg.waitForSyncFinishedThenSync();
if (event && event.cancelled) return;
setShareState(ShareState.Idle);
}
useAsyncEffect(async (event: AsyncEffectEvent) => {
const f = await Folder.load(props.folderId);
if (event.cancelled) return;
setFolder(f);
}, [props.folderId]);
useEffect(() => {
void ShareService.instance().refreshShares();
}, []);
useAsyncEffect(async (event: AsyncEffectEvent) => {
await synchronize(event);
}, []);
useEffect(() => {
const s = props.shares.find(s => s.folder_id === props.folderId);
setShare(s);
}, [props.shares]);
useEffect(() => {
if (!share) return;
void ShareService.instance().refreshShareUsers(share.id);
}, [share]);
useEffect(() => {
setCustomButtons(share ? [{
name: 'unshare',
label: _('Unshare'),
}] : []);
}, [share]);
useEffect(() => {
if (!share) return;
const sus = props.shareUsers[share.id];
if (!sus) return;
setShareUsers(sus);
}, [share, props.shareUsers]);
useEffect(() => {
void ShareService.instance().refreshShares();
}, [props.folderId]);
async function shareRecipient_click() {
setShareState(ShareState.Creating);
try {
setLatestError(null);
const share = await ShareService.instance().shareFolder(props.folderId);
await ShareService.instance().addShareRecipient(share.id, recipientEmail);
await Promise.all([
ShareService.instance().refreshShares(),
ShareService.instance().refreshShareUsers(share.id),
]);
setRecipientEmail('');
await synchronize();
} catch (error) {
logger.error(error);
setLatestError(error);
} finally {
setShareState(ShareState.Idle);
}
}
function recipientEmail_change(event: any) {
setRecipientEmail(event.target.value);
}
async function recipient_delete(event: RecipientDeleteEvent) {
if (!confirm(_('Delete this invitation? The recipient will no longer have access to this shared notebook.'))) return;
await ShareService.instance().deleteShareRecipient(event.shareUserId);
await ShareService.instance().refreshShareUsers(share.id);
}
function renderFolder() {
return (
<StyledFolder>
<StyledIcon className="icon-notebooks"/>{folder ? folder.title : '...'}
</StyledFolder>
);
}
function renderAddRecipient() {
const disabled = shareState !== ShareState.Idle;
return (
<StyledAddRecipient>
<StyledFormLabel>{_('Add recipient:')}</StyledFormLabel>
<StyledRecipientControls>
<StyledRecipientInput disabled={disabled} type="email" placeholder="example@domain.com" value={recipientEmail} onChange={recipientEmail_change} />
<Button disabled={disabled} title={_('Share')} onClick={shareRecipient_click}></Button>
</StyledRecipientControls>
</StyledAddRecipient>
);
}
function renderRecipient(index: number, shareUser: StateShareUser) {
const statusToIcon = {
[ShareUserStatus.Waiting]: 'fas fa-question',
[ShareUserStatus.Rejected]: 'fas fa-times',
[ShareUserStatus.Accepted]: 'fas fa-check',
};
const statusToMessage = {
[ShareUserStatus.Waiting]: _('Recipient has not yet accepted the invitation'),
[ShareUserStatus.Rejected]: _('Recipient has rejected the invitation'),
[ShareUserStatus.Accepted]: _('Recipient has accepted the invitation'),
};
return (
<StyledRecipient key={shareUser.user.email} index={index}>
<StyledRecipientName>{shareUser.user.email}</StyledRecipientName>
<StyledRecipientStatusIcon title={statusToMessage[shareUser.status]} className={statusToIcon[shareUser.status]}></StyledRecipientStatusIcon>
<Button iconName="far fa-times-circle" onClick={() => recipient_delete({ shareUserId: shareUser.id })}/>
</StyledRecipient>
);
}
function renderRecipients() {
const listItems = shareUsers.map((su: StateShareUser, index: number) => renderRecipient(index, su));
return (
<StyledRecipients>
<StyledFormLabel>{_('Recipients:')}</StyledFormLabel>
<StyledRecipientList>
{listItems}
</StyledRecipientList>
</StyledRecipients>
);
}
function renderError() {
if (!latestError) return null;
return (
<StyledError type="error">
{latestError.message}
</StyledError>
);
}
function renderShareState() {
if (shareState === ShareState.Idle) return null;
const messages = {
[ShareState.Synchronizing]: _('Synchronizing...'),
[ShareState.Creating]: _('Sharing notebook...'),
};
const message = messages[shareState];
if (!message) throw new Error(`Unsupported state: ${shareState}`);
return (
<StyledShareState>
{message}
</StyledShareState>
);
}
async function buttonRow_click(event: ClickEvent) {
if (event.buttonName === 'unshare') {
if (!confirm(_('Unshare this notebook? The recipients will no longer have access to its content.'))) return;
await ShareService.instance().unshareFolder(props.folderId);
void synchronize();
}
props.onClose();
}
function renderContent() {
return (
<div>
<DialogTitle title={_('Share Notebook')}/>
{renderFolder()}
{renderAddRecipient()}
{renderShareState()}
{renderError()}
{renderRecipients()}
<DialogButtonRow
themeId={props.themeId}
onClick={buttonRow_click}
okButtonShow={false}
cancelButtonLabel={_('Close')}
customButtons={customButtons}
/>
</div>
);
}
return (
<Dialog renderContent={renderContent}/>
);
}
const mapStateToProps = (state: State) => {
return {
shares: state.shareService.shares,
shareUsers: state.shareService.shareUsers,
};
};
export default connect(mapStateToProps)(ShareFolderDialog as any);

View File

@@ -4,12 +4,12 @@ import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import BaseItem from '@joplin/lib/models/BaseItem';
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
const { themeStyle, buildStyle } = require('@joplin/lib/theme');
const DialogButtonRow = require('./DialogButtonRow.min');
import DialogButtonRow from './DialogButtonRow';
import { themeStyle, buildStyle } from '@joplin/lib/theme';
import { reg } from '@joplin/lib/registry';
import Dialog from './Dialog';
import DialogTitle from './DialogTitle';
import ShareService from '@joplin/lib/services/share/ShareService';
const { clipboard } = require('electron');
interface ShareNoteDialogProps {
@@ -83,26 +83,19 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
void fetchNotes();
}, [props.noteIds]);
const fileApi = async () => {
const syncTarget = reg.syncTarget() as SyncTargetJoplinServer;
return syncTarget.fileApi();
};
const joplinServerApi = async (): Promise<JoplinServerApi> => {
return (await fileApi()).driver().api();
};
const buttonRow_click = () => {
props.onClose();
};
const copyLinksToClipboard = (api: JoplinServerApi, shares: SharesMap) => {
const copyLinksToClipboard = (shares: SharesMap) => {
const links = [];
for (const n in shares) links.push(api.shareUrl(shares[n]));
for (const n in shares) links.push(ShareService.instance().shareUrl(shares[n]));
clipboard.writeText(links.join('\n'));
};
const shareLinkButton_click = async () => {
const service = ShareService.instance();
let hasSynced = false;
let tryToSync = false;
while (true) {
@@ -116,29 +109,20 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
setSharesState('creating');
const api = await joplinServerApi();
const newShares = Object.assign({}, shares);
let sharedStatusChanged = false;
for (const note of notes) {
const fullPath = (await fileApi()).fullPath(BaseItem.systemPath(note.id));
const share = await api.shareFile(fullPath);
const share = await service.shareNote(note.id);
newShares[note.id] = share;
const changed = await BaseItem.updateShareStatus(note, true);
if (changed) sharedStatusChanged = true;
}
setShares(newShares);
if (sharedStatusChanged) {
setSharesState('synchronizing');
await reg.waitForSyncFinishedThenSync();
setSharesState('creating');
}
setSharesState('synchronizing');
await reg.waitForSyncFinishedThenSync();
setSharesState('creating');
copyLinksToClipboard(api, newShares);
copyLinksToClipboard(newShares);
setSharesState('created');
} catch (error) {
@@ -202,24 +186,20 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
}
function renderBetaWarningMessage() {
return <div style={theme.textStyle}>{'Sharing notes via Joplin Server is a Beta feature and the API might change later on. What it means is that if you share a note, the link might become invalid after an upgrade, and you will have to share it again.'}</div>;
}
const rootStyle = Object.assign({}, theme.dialogBox);
rootStyle.width = '50%';
return (
<div style={theme.dialogModalLayer}>
<div style={rootStyle}>
<div style={theme.dialogTitle}>{_('Share Notes')}</div>
function renderContent() {
return (
<div>
<DialogTitle title={_('Share Notes')}/>
{renderNoteList(notes)}
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{renderEncryptionWarningMessage()}
{renderBetaWarningMessage()}
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div>
</div>
);
}
return (
<Dialog renderContent={renderContent}/>
);
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { StyledRoot, StyledAddButton, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
import { ButtonLevel } from '../Button/Button';
import CommandService from '@joplin/lib/services/CommandService';
import InteropService from '@joplin/lib/services/interop/InteropService';
@@ -12,13 +12,16 @@ import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import { AppState } from '../../app';
import { ModelType } from '@joplin/lib/BaseModel';
const { connect } = require('react-redux');
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
import BaseModel from '@joplin/lib/BaseModel';
import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import Tag from '@joplin/lib/models/Tag';
import Logger from '@joplin/lib/Logger';
import { FolderEntity } from '@joplin/lib/services/database/types';
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
import { store } from '@joplin/lib/reducer';
const { connect } = require('react-redux');
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
const { themeStyle } = require('@joplin/lib/theme');
const bridge = require('electron').remote.require('./bridge').default;
const Menu = bridge().Menu;
@@ -26,6 +29,8 @@ const MenuItem = bridge().MenuItem;
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
const logger = Logger.create('Sidebar');
interface Props {
themeId: number;
dispatch: Function;
@@ -70,10 +75,12 @@ function ExpandLink(props: any) {
}
function FolderItem(props: any) {
const { hasChildren, isExpanded, depth, selected, folderId, folderTitle, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_ } = props;
const { hasChildren, isExpanded, parentId, depth, selected, folderId, folderTitle, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
const noteCountComp = noteCount ? <StyledNoteCount>{noteCount}</StyledNoteCount> : null;
const shareIcon = shareId && !parentId ? <StyledShareIcon className="fas fa-share-alt"></StyledShareIcon> : 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}/>
@@ -83,6 +90,7 @@ function FolderItem(props: any) {
isConflictFolder={folderId === Folder.conflictFolderId()}
href="#"
selected={selected}
shareId={shareId}
data-id={folderId}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={itemContextMenu}
@@ -92,7 +100,7 @@ function FolderItem(props: any) {
}}
onDoubleClick={onFolderToggleClick_}
>
{folderTitle} {noteCountComp}
{folderTitle} {shareIcon} {noteCountComp}
</StyledListItemAnchor>
</StyledListItem>
);
@@ -158,22 +166,27 @@ class SidebarComponent extends React.Component<Props, State> {
// to put the dropped folder at the root. But for notes, folderId needs to always be defined
// since there's no such thing as a root note.
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
event.preventDefault();
try {
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
event.preventDefault();
if (!folderId) return;
if (!folderId) return;
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId);
}
} else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) {
event.preventDefault();
const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids'));
for (let i = 0; i < folderIds.length; i++) {
await Folder.moveToFolder(folderIds[i], folderId);
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId);
}
} else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) {
event.preventDefault();
const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids'));
for (let i = 0; i < folderIds.length; i++) {
await Folder.moveToFolder(folderIds[i], folderId);
}
}
} catch (error) {
logger.error(error);
alert(error.message);
}
}
@@ -222,12 +235,14 @@ class SidebarComponent extends React.Component<Props, State> {
const itemType = Number(event.currentTarget.getAttribute('data-type'));
if (!itemId || !itemType) throw new Error('No data on element');
const state: AppState = store().getState();
let deleteMessage = '';
let buttonLabel = _('Remove');
let deleteButtonLabel = _('Remove');
if (itemType === BaseModel.TYPE_FOLDER) {
const folder = await Folder.load(itemId);
deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32));
buttonLabel = _('Delete');
deleteButtonLabel = _('Delete');
} else if (itemType === BaseModel.TYPE_TAG) {
const tag = await Tag.load(itemId);
deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32));
@@ -250,10 +265,10 @@ class SidebarComponent extends React.Component<Props, State> {
menu.append(
new MenuItem({
label: buttonLabel,
label: deleteButtonLabel,
click: async () => {
const ok = bridge().showConfirmMessageBox(deleteMessage, {
buttons: [buttonLabel, _('Cancel')],
buttons: [deleteButtonLabel, _('Cancel')],
defaultId: 1,
});
if (!ok) return;
@@ -294,6 +309,14 @@ class SidebarComponent extends React.Component<Props, State> {
);
}
// We don't display the "Share notebook" menu item for sub-notebooks
// that are within a shared notebook. If user wants to do this,
// they'd have to move the notebook out of the shared notebook
// first.
if (CommandService.instance().isEnabled('showShareFolderDialog', stateToWhenClauseContext(state, { commandFolderId: itemId }))) {
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('showShareFolderDialog', itemId)));
}
menu.append(
new MenuItem({
label: _('Export'),
@@ -387,10 +410,10 @@ class SidebarComponent extends React.Component<Props, State> {
);
}
renderFolderItem(folder: any, selected: boolean, hasChildren: boolean, depth: number) {
renderFolderItem(folder: FolderEntity, selected: boolean, hasChildren: boolean, depth: number) {
const anchorRef = this.anchorItemRef('folder', folder.id);
const isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0;
let noteCount = folder.note_count;
let noteCount = (folder as any).note_count;
// Thunderbird count: Subtract children note_count from parent folder if it expanded.
if (isExpanded) {
@@ -418,6 +441,8 @@ class SidebarComponent extends React.Component<Props, State> {
itemContextMenu={this.itemContextMenu}
folderItem_click={this.folderItem_click}
onFolderToggleClick_={this.onFolderToggleClick_}
shareId={folder.share_id}
parentId={folder.parent_id}
/>;
}

View File

@@ -63,6 +63,7 @@ export const StyledListItem = styled.div`
function listItemTextColor(props: any) {
if (props.isConflictFolder) return props.theme.colorError2;
if (props.isSpecialItem) return props.theme.colorFaded2;
if (props.shareId) return props.theme.colorWarn2;
return props.theme.color2;
}
@@ -72,7 +73,7 @@ export const StyledListItemAnchor = styled.a`
text-decoration: none;
color: ${(props: any) => listItemTextColor(props)};
cursor: default;
opacity: ${(props: any) => props.selected ? 1 : 0.8};
opacity: ${(props: any) => props.selected || props.shareId ? 1 : 0.8};
white-space: nowrap;
display: flex;
flex: 1;
@@ -81,6 +82,10 @@ export const StyledListItemAnchor = styled.a`
height: 100%;
`;
export const StyledShareIcon = styled.i`
margin-left: 8px;
`;
export const StyledExpandLink = styled.a`
color: ${(props: any) => props.theme.color2};
cursor: default;

View File

@@ -0,0 +1,11 @@
import styled from 'styled-components';
const StyledFormLabel = styled.div`
font-size: ${props => props.theme.fontSize * 1.083333}px;
color: ${props => props.theme.color};
font-family: ${props => props.theme.fontFamily};
font-weight: 500;
margin-bottom: ${props => props.theme.mainPadding / 2}px;
`;
export default StyledFormLabel;

View File

@@ -0,0 +1,12 @@
import styled from 'styled-components';
const StyledMessage = styled.div`
border-radius: 3px;
background-color: ${props => props.type === 'error' ? props.theme.warningBackgroundColor : 'transparent'};
font-size: ${props => props.theme.fontSize}px;
color: ${props => props.theme.color};
font-family: ${props => props.theme.fontFamily};
padding: ${props => props.type === 'error' ? props.theme.mainPadding : '0'}px;
`;
export default StyledMessage;

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.8.4",
"version": "1.8.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3199,7 +3199,7 @@
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"resolved": false,
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true,
"optional": true
@@ -7659,7 +7659,7 @@
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"resolved": false,
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true,
"optional": true

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.8.4",
"version": "2.0.0",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Setup the sync parameters for user X and create a few folders and notes to
# allow sharing. Also calls the API to create the test users and clear the data.
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ROOT_DIR="$SCRIPT_DIR/../.."
if [ "$1" == "" ]; then
echo "User number is required"
exit 1
fi
USER_NUM=$1
CMD_FILE="$SCRIPT_DIR/runForSharingCommands-$USER_NUM.txt"
rm -f "$CMD_FILE"
USER_EMAIL="user$USER_NUM@example.com"
PROFILE_DIR=~/.config/joplindev-desktop-$USER_NUM
rm -rf "$PROFILE_DIR"
echo "config keychain.supported 0" >> "$CMD_FILE"
echo "config sync.target 9" >> "$CMD_FILE"
echo "config sync.9.path http://localhost:22300" >> "$CMD_FILE"
echo "config sync.9.username $USER_EMAIL" >> "$CMD_FILE"
echo "config sync.9.password 123456" >> "$CMD_FILE"
if [ "$1" == "1" ]; then
curl --data '{"action": "createTestUsers"}' http://localhost:22300/api/debug
echo 'mkbook "shared"' >> "$CMD_FILE"
echo 'mkbook "other"' >> "$CMD_FILE"
echo 'use "shared"' >> "$CMD_FILE"
echo 'mknote "note 1"' >> "$CMD_FILE"
fi
cd "$ROOT_DIR/packages/app-cli"
npm start -- --profile "$PROFILE_DIR" batch "$CMD_FILE"
cd "$ROOT_DIR/packages/app-desktop"
npm start -- --profile "$PROFILE_DIR"

View File

@@ -4,12 +4,12 @@
// one.
import { AppState } from '../../app';
import libStateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
import libStateToWhenClauseContext, { WhenClauseContextOptions } from '@joplin/lib/services/commands/stateToWhenClauseContext';
import layoutItemProp from '../../gui/ResizableLayout/utils/layoutItemProp';
export default function stateToWhenClauseContext(state: AppState) {
export default function stateToWhenClauseContext(state: AppState, options: WhenClauseContextOptions = null) {
return {
...libStateToWhenClauseContext(state),
...libStateToWhenClauseContext(state, options),
// UI elements
markdownEditorVisible: !!state.settings['editor.codeView'],

View File

@@ -142,7 +142,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097631
versionName "1.8.5"
versionName "2.0.0"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -372,7 +372,7 @@ class SideMenuContentComponent extends Component {
items.push(this.renderSidebarButton('folder_header', _('Notebooks'), 'md-folder'));
if (this.props.folders.length) {
const result = shared.renderFolders(this.props, this.renderFolderItem);
const result = shared.renderFolders(this.props, this.renderFolderItem, false);
const folderItems = result.items;
items = items.concat(folderItems);
}

View File

@@ -266,6 +266,7 @@
AE82E4A72599FA3A0013551B = {
CreatedOnToolsVersion = 12.0.1;
DevelopmentTeam = A9BXAFS6CT;
ProvisioningStyle = Automatic;
};
};
};
@@ -485,13 +486,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 63;
CURRENT_PROJECT_VERSION = 65;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 10.8.0;
MARKETING_VERSION = 20.0.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -513,12 +514,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 63;
CURRENT_PROJECT_VERSION = 65;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 10.8.0;
MARKETING_VERSION = 20.0.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -658,12 +659,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 65;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 20.0.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -687,12 +690,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 65;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 20.0.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
@@ -33,7 +33,7 @@
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsText</key>
<integer>1</integer>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>

View File

@@ -36,7 +36,7 @@ const SafeAreaView = require('./components/SafeAreaView');
const { connect, Provider } = require('react-redux');
const { BackButtonService } = require('./services/back-button.js');
import NavService from '@joplin/lib/services/NavService';
const { createStore, applyMiddleware } = require('redux');
import { createStore, applyMiddleware } from 'redux';
const reduxSharedMiddleware = require('@joplin/lib/components/shared/reduxSharedMiddleware');
const { shimInit } = require('./utils/shim-init-react.js');
const { AppNav } = require('./components/app-nav.js');
@@ -97,6 +97,7 @@ import EncryptionService from '@joplin/lib/services/EncryptionService';
import MigrationService from '@joplin/lib/services/MigrationService';
import { clearSharedFilesCache } from './utils/ShareUtils';
import setIgnoreTlsErrors from './utils/TlsUtils';
import ShareService from '@joplin/lib/services/share/ShareService';
let storeDispatch = function(_action: any) {};
@@ -525,6 +526,7 @@ async function initialize(dispatch: Function) {
EncryptionService.instance().setLogger(mainLogger);
// eslint-disable-next-line require-atomic-updates
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
DecryptionWorker.instance().dispatch = dispatch;
DecryptionWorker.instance().setLogger(mainLogger);
DecryptionWorker.instance().setKvStore(KvStore.instance());
@@ -536,6 +538,8 @@ async function initialize(dispatch: Function) {
// / E2EE SETUP
// ----------------------------------------------------------------
await ShareService.instance().initialize(store);
reg.logger().info('Loading folders...');
await FoldersScreenUtils.refreshFolders();

View File

@@ -1,4 +1,4 @@
import { Platform, NativeModules } from 'react-native';
const { Platform, NativeModules } = require('react-native');
export default async function setIgnoreTlsErrors(ignore: boolean): Promise<boolean> {
if (Platform.OS === 'android') {

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"id": "<%= pluginId %>",
"app_min_version": "1.8",
"app_min_version": "2.0",
"version": "1.0.0",
"name": "<%= pluginName %>",
"description": "<%= pluginDescription %>",

View File

@@ -41,7 +41,7 @@ function mergePackageKey(parentKey, source, dest) {
}
function mergeIgnoreFile(source, dest) {
const output = dest.trim().split('\n').concat(source.trim().split('\n'));
const output = dest.trim().split(/\r?\n/).concat(source.trim().split(/\r?\n/));
return `${output.filter(function(item, pos) {
if (!item) return true; // Leave blank lines

View File

@@ -33,6 +33,9 @@ describe('utils', () => {
const testCases = [
['line1\nline2\n', 'newline\n', 'line1\nline2\nnewline\n'],
['line1\nline2\n', 'line1\nnewline\n', 'line1\nline2\nnewline\n'],
['line1\r\nline2\r\n', 'line1\nnewline\n', 'line1\nline2\nnewline\n'],
['line1\nline2\n', 'line1\r\nnewline\r\n', 'line1\nline2\nnewline\n'],
['line1\r\nline2\r\n', 'line1\r\nnewline\r\n', 'line1\nline2\nnewline\n'],
];
for (const t of testCases) {

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "1.8.0",
"version": "1.8.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "1.8.0",
"version": "2.0.0",
"description": "Scaffolds out a new Joplin plugin",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
"author": {

View File

@@ -2,14 +2,14 @@ import Setting from './models/Setting';
import Logger, { TargetType, LoggerWrapper } from './Logger';
import shim from './shim';
import BaseService from './services/BaseService';
import reducer from './reducer';
import reducer, { setStore } from './reducer';
import KeychainServiceDriver from './services/keychain/KeychainServiceDriver.node';
import { _, setLocale } from './locale';
import KvStore from './services/KvStore';
import SyncTargetJoplinServer from './SyncTargetJoplinServer';
import SyncTargetOneDrive from './SyncTargetOneDrive';
const { createStore, applyMiddleware } = require('redux');
import { createStore, applyMiddleware, Store } from 'redux';
const { defaultState, stateUtils } = require('./reducer');
import JoplinDatabase from './JoplinDatabase';
const { FoldersScreenUtils } = require('./folders-screen-utils.js');
@@ -26,7 +26,7 @@ import BaseSyncTarget from './BaseSyncTarget';
const reduxSharedMiddleware = require('./components/shared/reduxSharedMiddleware');
const os = require('os');
const fs = require('fs-extra');
const JoplinError = require('./JoplinError');
import JoplinError from './JoplinError';
const EventEmitter = require('events');
const syswidecas = require('./vendor/syswide-cas');
const SyncTargetRegistry = require('./SyncTargetRegistry.js');
@@ -44,6 +44,7 @@ import ResourceService from './services/ResourceService';
import DecryptionWorker from './services/DecryptionWorker';
const { loadKeychainServiceAndSettings } = require('./services/SettingUtils');
import MigrationService from './services/MigrationService';
import ShareService from './services/share/ShareService';
import handleSyncStartupOperation from './services/synchronizer/utils/handleSyncStartupOperation';
const { toSystemSlashes } = require('./path-utils');
const { setAutoFreeze } = require('immer');
@@ -67,7 +68,7 @@ export default class BaseApplication {
// state and UI out of sync.
private currentFolder_: any = null;
protected store_: any = null;
protected store_: Store<any> = null;
constructor() {
this.eventEmitter_ = new EventEmitter();
@@ -602,13 +603,15 @@ export default class BaseApplication {
}
initRedux() {
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn()));
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn() as any));
setStore(this.store_);
BaseModel.dispatch = this.store().dispatch;
FoldersScreenUtils.dispatch = this.store().dispatch;
// reg.dispatch = this.store().dispatch;
BaseSyncTarget.dispatch = this.store().dispatch;
DecryptionWorker.instance().dispatch = this.store().dispatch;
ResourceFetcher.instance().dispatch = this.store().dispatch;
ShareService.instance().initialize(this.store());
}
deinitRedux() {
@@ -793,6 +796,7 @@ export default class BaseApplication {
EncryptionService.instance().setLogger(globalLogger);
BaseItem.encryptionService_ = EncryptionService.instance();
BaseItem.shareService_ = ShareService.instance();
DecryptionWorker.instance().setLogger(globalLogger);
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
DecryptionWorker.instance().setKvStore(KvStore.instance());

View File

@@ -3,6 +3,7 @@ import Synchronizer from './Synchronizer';
import EncryptionService from './services/EncryptionService';
import shim from './shim';
import ResourceService from './services/ResourceService';
import ShareService from './services/share/ShareService';
export default class BaseSyncTarget {
@@ -113,6 +114,7 @@ export default class BaseSyncTarget {
this.synchronizer_.setLogger(this.logger());
this.synchronizer_.setEncryptionService(EncryptionService.instance());
this.synchronizer_.setResourceService(ResourceService.instance());
this.synchronizer_.setShareService(ShareService.instance());
this.synchronizer_.dispatch = BaseSyncTarget.dispatch;
this.initState_ = 'ready';
return this.synchronizer_;

View File

@@ -1,6 +1,6 @@
const Logger = require('./Logger').default;
const shim = require('./shim').default;
const JoplinError = require('./JoplinError');
const JoplinError = require('./JoplinError').default;
const time = require('./time').default;
const EventDispatcher = require('./EventDispatcher');

View File

@@ -138,20 +138,20 @@ export default class JoplinDatabase extends Database {
private tableFieldNames_: Record<string, string[]> = {};
private tableDescriptions_: any;
constructor(driver: any) {
public constructor(driver: any) {
super(driver);
}
initialized() {
public initialized() {
return this.initialized_;
}
async open(options: any) {
public async open(options: any) {
await super.open(options);
return this.initialize();
}
tableFieldNames(tableName: string) {
public tableFieldNames(tableName: string) {
if (this.tableFieldNames_[tableName]) return this.tableFieldNames_[tableName].slice();
const tf = this.tableFields(tableName);
@@ -164,7 +164,7 @@ export default class JoplinDatabase extends Database {
return output.slice();
}
tableFields(tableName: string, options: any = null) {
public tableFields(tableName: string, options: any = null) {
if (options === null) options = {};
if (!this.tableFields_) throw new Error('Fields have not been loaded yet');
@@ -180,7 +180,7 @@ export default class JoplinDatabase extends Database {
return output;
}
async clearForTesting() {
public async clearForTesting() {
const tableNames = [
'notes',
'folders',
@@ -220,7 +220,7 @@ export default class JoplinDatabase extends Database {
await this.transactionExecBatch(queries);
}
createDefaultRow(tableName: string) {
public createDefaultRow(tableName: string) {
const row: any = {};
const fields = this.tableFields(tableName);
for (let i = 0; i < fields.length; i++) {
@@ -230,7 +230,7 @@ export default class JoplinDatabase extends Database {
return row;
}
fieldByName(tableName: string, fieldName: string) {
public fieldByName(tableName: string, fieldName: string) {
const fields = this.tableFields(tableName);
for (const field of fields) {
if (field.name === fieldName) return field;
@@ -238,11 +238,11 @@ export default class JoplinDatabase extends Database {
throw new Error(`No such field: ${tableName}: ${fieldName}`);
}
fieldDefaultValue(tableName: string, fieldName: string) {
public fieldDefaultValue(tableName: string, fieldName: string) {
return this.fieldByName(tableName, fieldName).default;
}
fieldDescription(tableName: string, fieldName: string) {
public fieldDescription(tableName: string, fieldName: string) {
const sp = sprintf;
if (!this.tableDescriptions_) {
@@ -278,7 +278,7 @@ export default class JoplinDatabase extends Database {
return d && d[fieldName] ? d[fieldName] : '';
}
refreshTableFields(newVersion: number) {
public refreshTableFields(newVersion: number) {
this.logger().info('Initializing tables...');
const queries: SqlQuery[] = [];
queries.push(this.wrapQuery('DELETE FROM table_fields'));
@@ -323,12 +323,12 @@ export default class JoplinDatabase extends Database {
});
}
addMigrationFile(num: number) {
public addMigrationFile(num: number) {
const timestamp = Date.now();
return { sql: 'INSERT INTO migrations (number, created_time, updated_time) VALUES (?, ?, ?)', params: [num, timestamp, timestamp] };
}
async upgradeDatabase(fromVersion: number) {
public async upgradeDatabase(fromVersion: number) {
// INSTRUCTIONS TO UPGRADE THE DATABASE:
//
// 1. Add the new version number to the existingDatabaseVersions array
@@ -343,7 +343,7 @@ export default class JoplinDatabase extends Database {
// must be set in the synchronizer too.
// Note: v16 and v17 don't do anything. They were used to debug an issue.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35];
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
@@ -870,6 +870,12 @@ export default class JoplinDatabase extends Database {
queries.push(this.addMigrationFile(35));
}
if (targetVersion == 36) {
queries.push('ALTER TABLE folders ADD COLUMN share_id TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE notes ADD COLUMN share_id TEXT NOT NULL DEFAULT ""');
queries.push('ALTER TABLE resources ADD COLUMN share_id TEXT NOT NULL DEFAULT ""');
}
const updateVersionQuery = { sql: 'UPDATE version SET version = ?', params: [targetVersion] };
queries.push(updateVersionQuery);
@@ -906,7 +912,7 @@ export default class JoplinDatabase extends Database {
return latestVersion;
}
async ftsEnabled() {
public async ftsEnabled() {
try {
await this.selectOne('SELECT count(*) FROM notes_fts');
} catch (error) {
@@ -919,7 +925,7 @@ export default class JoplinDatabase extends Database {
return true;
}
async fuzzySearchEnabled() {
public async fuzzySearchEnabled() {
try {
await this.selectOne('SELECT count(*) FROM notes_spellfix');
} catch (error) {
@@ -930,11 +936,11 @@ export default class JoplinDatabase extends Database {
return true;
}
version() {
public version() {
return this.version_;
}
async initialize() {
public async initialize() {
this.logger().info('Checking for database schema update...');
let versionRow = null;

View File

@@ -1,8 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class JoplinError extends Error {
constructor(message, code = null) {
super(message);
this.code = code;
}
constructor(message, code = null, details = null) {
super(message);
this.code = null;
this.details = '';
this.code = code;
this.details = details;
}
}
module.exports = JoplinError;
exports.default = JoplinError;
//# sourceMappingURL=JoplinError.js.map

View File

@@ -0,0 +1,12 @@
export default class JoplinError extends Error {
public code: any = null;
public details: string = '';
public constructor(message: string, code: any = null, details: string = null) {
super(message);
this.code = code;
this.details = details;
}
}

View File

@@ -1,13 +1,15 @@
import shim from './shim';
import { _ } from './locale';
const { rtrimSlashes } = require('./path-utils.js');
const JoplinError = require('./JoplinError');
import JoplinError from './JoplinError';
import { Env } from './models/Setting';
const { stringify } = require('query-string');
interface Options {
baseUrl(): string;
username(): string;
password(): string;
env?: Env;
}
enum ExecOptionsResponseFormat {
@@ -27,22 +29,30 @@ interface ExecOptions {
source?: string;
}
interface Session {
id: string;
user_id: string;
}
export default class JoplinServerApi {
private options_: Options;
private session_: any;
private session_: Session;
private debugRequests_: boolean = false;
public constructor(options: Options) {
this.options_ = options;
if (options.env === Env.Dev) {
this.debugRequests_ = true;
}
}
private baseUrl() {
public baseUrl() {
return rtrimSlashes(this.options_.baseUrl());
}
private async session() {
// TODO: handle invalid session
if (this.session_) return this.session_;
this.session_ = await this.exec('POST', 'api/sessions', null, {
@@ -58,11 +68,8 @@ export default class JoplinServerApi {
return session ? session.id : '';
}
public async shareFile(pathOrId: string) {
return this.exec('POST', 'api/shares', null, {
file_id: pathOrId,
type: 1, // ShareType.Link
});
public get userId(): string {
return this.session_ ? this.session_.user_id : '';
}
public static connectionErrorMessage(error: any) {
@@ -70,10 +77,6 @@ export default class JoplinServerApi {
return _('Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s', msg);
}
public shareUrl(share: any): string {
return `${this.baseUrl()}/shares/${share.id}`;
}
private requestToCurl_(url: string, options: any) {
const output = [];
output.push('curl');
@@ -85,8 +88,11 @@ export default class JoplinServerApi {
output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
}
}
if (options.body) output.push(`${'--data ' + '\''}${JSON.stringify(options.body)}'`);
output.push(url);
if (options.body) {
const serialized = typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body;
output.push(`${'--data ' + '\''}${serialized}'`);
}
output.push(`'${url}'`);
return output.join(' ');
}
@@ -150,14 +156,17 @@ export default class JoplinServerApi {
const responseText = await response.text();
// console.info('Joplin API Response', responseText);
if (this.debugRequests_) {
console.info('Joplin API Response', responseText);
}
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
const newError = (message: string, code: number = 0) => {
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = (`${responseText}`).substr(0, 1024);
return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
// return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
return new JoplinError(message, code, `${method} ${path}: ${message} (${code}): ${shortResponseText}`);
};
let responseJson_: any = null;

View File

@@ -10,7 +10,6 @@ interface FileApiOptions {
path(): string;
username(): string;
password(): string;
directory(): string;
}
export default class SyncTargetJoplinServer extends BaseSyncTarget {
@@ -28,7 +27,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
}
public static label() {
return _('Joplin Server');
return `${_('Joplin Server')} (Beta)`;
}
public async isAuthenticated() {
@@ -44,11 +43,12 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
baseUrl: () => options.path(),
username: () => options.username(),
password: () => options.password(),
env: Setting.value('env'),
};
const api = new JoplinServerApi(apiOptions);
const driver = new FileApiDriverJoplinServer(api);
const fileApi = new FileApi(options.directory, driver);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(this.id());
await fileApi.initialize();
return fileApi;
@@ -64,8 +64,10 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
const fileApi = await SyncTargetJoplinServer.newFileApi_(options);
fileApi.requestRepeatCount_ = 0;
const result = await fileApi.stat('');
if (!result) throw new Error(`Sync directory not found: "${options.directory()}" on server "${options.path()}"`);
await fileApi.put('testing.txt', 'testing');
const result = await fileApi.get('testing.txt');
if (result !== 'testing') throw new Error(`Could not access data on server "${options.path()}"`);
await fileApi.delete('testing.txt');
output.ok = true;
} catch (error) {
output.errorMessage = error.message;
@@ -80,7 +82,6 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
path: () => Setting.value('sync.9.path'),
username: () => Setting.value('sync.9.username'),
password: () => Setting.value('sync.9.password'),
directory: () => Setting.value('sync.9.directory'),
});
fileApi.setLogger(this.logger());

View File

@@ -5,7 +5,6 @@ import shim from './shim';
import MigrationHandler from './services/synchronizer/MigrationHandler';
import eventManager from './eventManager';
import { _ } from './locale';
import BaseItem from './models/BaseItem';
import Folder from './models/Folder';
import Note from './models/Note';
@@ -14,12 +13,12 @@ import ItemChange from './models/ItemChange';
import ResourceLocalState from './models/ResourceLocalState';
import MasterKey from './models/MasterKey';
import BaseModel from './BaseModel';
const { sprintf } = require('sprintf-js');
import time from './time';
import ResourceService from './services/ResourceService';
import EncryptionService from './services/EncryptionService';
import NoteResource from './models/NoteResource';
const JoplinError = require('./JoplinError');
import JoplinError from './JoplinError';
import ShareService from './services/share/ShareService';
const { sprintf } = require('sprintf-js');
const TaskQueue = require('./TaskQueue');
const { Dirnames } = require('./services/synchronizer/utils/types');
@@ -45,6 +44,7 @@ export default class Synchronizer {
private encryptionService_: EncryptionService = null;
private resourceService_: ResourceService = null;
private syncTargetIsLocked_: boolean = false;
private shareService_: ShareService = null;
// Debug flags are used to test certain hard-to-test conditions
// such as cancelling in the middle of a loop.
@@ -108,6 +108,10 @@ export default class Synchronizer {
return this.appType_ === 'mobile' ? 100 * 1000 * 1000 : Infinity;
}
public setShareService(v: ShareService) {
this.shareService_ = v;
}
public setEncryptionService(v: any) {
this.encryptionService_ = v;
}
@@ -351,12 +355,15 @@ export default class Synchronizer {
if (this.resourceService()) {
this.logger().info('Indexing resources...');
await this.resourceService().indexNoteResources();
await NoteResource.applySharedStatusToLinkedResources();
}
} catch (error) {
this.logger().error('Error indexing resources:', error);
}
// Before synchronising make sure all share_id properties are set
// correctly so as to share/unshare the right items.
await Folder.updateAllShareIds();
let errorToThrow = null;
let syncLock = null;
@@ -505,7 +512,7 @@ export default class Synchronizer {
this.logger().warn(`Uploading a large resource (resourceId: ${local.id}, size:${local.size} bytes) which may tie up the sync process.`);
}
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file', shareId: local.share_id });
} catch (error) {
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
@@ -924,6 +931,16 @@ export default class Synchronizer {
this.cancelling_ = false;
}
// After syncing, we run the share service maintenance, which is going
// to fetch share invitations, if any.
if (this.shareService_) {
try {
await this.shareService_.maintenance();
} catch (error) {
this.logger().error('Could not run share service maintenance:', error);
}
}
this.progressReport_.completedTime = time.unixMs();
this.logSyncOperation('finished', null, null, `Synchronisation finished [${synchronizationId}]`);

View File

@@ -1,7 +1,7 @@
const Logger = require('./Logger').default;
const shim = require('./shim').default;
const parseXmlString = require('xml2js').parseString;
const JoplinError = require('./JoplinError');
const JoplinError = require('./JoplinError').default;
const URL = require('url-parse');
const { rtrimSlashes } = require('./path-utils');
const base64 = require('base-64');

View File

@@ -4,7 +4,7 @@ import shim from './shim';
const Mutex = require('async-mutex').Mutex;
type SqlParams = Record<string, any>;
type SqlParams = any[];
export interface SqlQuery {
sql: string;

View File

@@ -0,0 +1,47 @@
import JoplinDatabase from '../JoplinDatabase';
import Setting from '../models/Setting';
import SyncTargetJoplinServer from '../SyncTargetJoplinServer';
export default class DebugService {
private db_: JoplinDatabase;
public constructor(db: JoplinDatabase) {
this.db_ = db;
}
private get db(): JoplinDatabase {
return this.db_;
}
public async clearSyncState() {
const tableNames = [
'item_changes',
'deleted_items',
'sync_items',
'key_values',
];
const queries = [];
for (const n of tableNames) {
queries.push(`DELETE FROM ${n}`);
queries.push(`DELETE FROM sqlite_sequence WHERE name="${n}"`); // Reset autoincremented IDs
}
for (let i = 0; i < 20; i++) {
queries.push(`DELETE FROM settings WHERE key="sync.${i}.context"`);
queries.push(`DELETE FROM settings WHERE key="sync.${i}.auth"`);
}
await this.db.transactionExecBatch(queries);
}
public async setupJoplinServerUser(num: number) {
const id = SyncTargetJoplinServer.id();
Setting.setValue('sync.target', id);
Setting.setValue(`sync.${id}.path`, 'http://localhost:22300');
Setting.setValue(`sync.${id}.username`, `user${num}@example.com`);
Setting.setValue(`sync.${id}.password`, '123456');
}
}

View File

@@ -1,7 +1,7 @@
const { basicDelta } = require('./file-api');
const { basename } = require('./path-utils');
const shim = require('./shim').default;
const JoplinError = require('./JoplinError');
const JoplinError = require('./JoplinError').default;
const { Buffer } = require('buffer');
const S3_MAX_DELETES = 1000;

View File

@@ -1,6 +1,6 @@
const time = require('./time').default;
const shim = require('./shim').default;
const JoplinError = require('./JoplinError');
const JoplinError = require('./JoplinError').default;
class FileApiDriverDropbox {
constructor(api) {

View File

@@ -1,5 +1,5 @@
import JoplinServerApi from './JoplinServerApi';
import { trimSlashes, dirname, basename } from './path-utils';
import { trimSlashes } from './path-utils';
// All input paths should be in the format: "path/to/file". This is converted to
// "root:/path/to/file:" when doing the API call.
@@ -34,38 +34,35 @@ export default class FileApiDriverJoplinServer {
return 3;
}
private metadataToStat_(md: any, path: string, isDeleted: boolean = false) {
private metadataToStat_(md: any, path: string, isDeleted: boolean = false, rootPath: string) {
const output = {
path: path,
path: rootPath ? path.substr(rootPath.length + 1) : path,
updated_time: md.updated_time,
isDir: !!md.is_directory,
isDir: false, // !!md.is_directory,
isDeleted: isDeleted,
};
// TODO - HANDLE DELETED
// if (md['.tag'] === 'deleted') output.isDeleted = true;
return output;
}
private metadataToStats_(mds: any[]) {
private metadataToStats_(mds: any[], rootPath: string) {
const output = [];
for (let i = 0; i < mds.length; i++) {
output.push(this.metadataToStat_(mds[i], mds[i].name));
output.push(this.metadataToStat_(mds[i], mds[i].name, false, rootPath));
}
return output;
}
// Transforms a path such as "Apps/Joplin/file.txt" to a complete a complete
// API URL path: "api/files/root:/Apps/Joplin/file.txt:"
// API URL path: "api/items/root:/Apps/Joplin/file.txt:"
private apiFilePath_(p: string) {
return `api/files/root:/${trimSlashes(p)}:`;
return `api/items/root:/${trimSlashes(p)}:`;
}
public async stat(path: string) {
try {
const response = await this.api().exec('GET', this.apiFilePath_(path));
return this.metadataToStat_(response, path);
return this.metadataToStat_(response, path, false, '');
} catch (error) {
if (error.code === 404) return null;
throw error;
@@ -80,9 +77,13 @@ export default class FileApiDriverJoplinServer {
try {
const query = cursor ? { cursor } : {};
const response = await this.api().exec('GET', `${this.apiFilePath_(path)}/delta`, query);
const stats = response.items.map((item: any) => {
return this.metadataToStat_(item.item, item.item.name, item.type === 3);
});
const stats = response.items
.filter((item: any) => {
return item.item_name.indexOf('locks/') !== 0 && item.item_name.indexOf('temp/') !== 0;
})
.map((item: any) => {
return this.metadataToStat_(item, item.item_name, item.type === 3, '');
});
const output = {
items: stats,
@@ -108,15 +109,22 @@ export default class FileApiDriverJoplinServer {
...options,
};
let isUsingWildcard = false;
let searchPath = path;
if (searchPath) {
searchPath += '/*';
isUsingWildcard = true;
}
const query = options.context?.cursor ? { cursor: options.context.cursor } : null;
const results = await this.api().exec('GET', `${this.apiFilePath_(path)}/children`, query);
const results = await this.api().exec('GET', `${this.apiFilePath_(searchPath)}/children`, query);
const newContext: any = {};
if (results.cursor) newContext.cursor = results.cursor;
return {
items: this.metadataToStats_(results.items),
items: this.metadataToStats_(results.items, isUsingWildcard ? path : ''),
hasMore: results.has_more,
context: newContext,
} as any;
@@ -134,32 +142,13 @@ export default class FileApiDriverJoplinServer {
}
}
private parentPath_(path: string) {
return dirname(path);
}
private basename_(path: string) {
return basename(path);
}
public async mkdir(path: string) {
const parentPath = this.parentPath_(path);
const filename = this.basename_(path);
try {
const response = await this.api().exec('POST', `${this.apiFilePath_(parentPath)}/children`, null, {
name: filename,
is_directory: 1,
});
return response;
} catch (error) {
// 409 is OK - directory already exists
if (error.code !== 409) throw error;
}
public async mkdir(_path: string) {
// This is a no-op because all items technically are at the root, but
// they can have names such as ".resources/xxxxxxxxxx'
}
public async put(path: string, content: any, options: any = null) {
return this.api().exec('PUT', `${this.apiFilePath_(path)}/content`, null, content, {
return this.api().exec('PUT', `${this.apiFilePath_(path)}/content`, options && options.shareId ? { share_id: options.shareId } : null, content, {
'Content-Type': 'application/octet-stream',
}, options);
}
@@ -174,6 +163,5 @@ export default class FileApiDriverJoplinServer {
public async clearRoot(path: string) {
await this.delete(path);
await this.mkdir(path);
}
}

View File

@@ -1,6 +1,6 @@
const { basicDelta } = require('./file-api');
const { rtrimSlashes, ltrimSlashes } = require('./path-utils');
const JoplinError = require('./JoplinError');
const JoplinError = require('./JoplinError').default;
class FileApiDriverWebDav {
constructor(api) {

View File

@@ -4,7 +4,7 @@ import BaseItem from './models/BaseItem';
import time from './time';
const { isHidden } = require('./path-utils');
const JoplinError = require('./JoplinError');
import JoplinError from './JoplinError';
const ArrayUtils = require('./ArrayUtils');
const { sprintf } = require('sprintf-js');
const Mutex = require('async-mutex').Mutex;
@@ -14,6 +14,10 @@ const logger = Logger.create('FileApi');
function requestCanBeRepeated(error: any) {
const errorCode = typeof error === 'object' && error.code ? error.code : null;
// Unauthorized error - means username or password is incorrect or other
// permission issue, which won't be fixed by repeating the request.
if (errorCode === 403) return false;
// The target is explicitely rejecting the item so repeating wouldn't make a difference.
if (errorCode === 'rejectedByTarget') return false;

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

View File

@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
locales['vi'] = require('./vi.json');
locales['zh_CN'] = require('./zh_CN.json');
locales['zh_TW'] = require('./zh_TW.json');
stats['ar'] = {"percentDone":99};
stats['ar'] = {"percentDone":98};
stats['eu'] = {"percentDone":31};
stats['bs_BA'] = {"percentDone":74};
stats['bg_BG'] = {"percentDone":60};
stats['ca'] = {"percentDone":85};
stats['bs_BA'] = {"percentDone":76};
stats['bg_BG'] = {"percentDone":59};
stats['ca'] = {"percentDone":84};
stats['hr_HR'] = {"percentDone":99};
stats['cs_CZ'] = {"percentDone":89};
stats['da_DK'] = {"percentDone":96};
stats['cs_CZ'] = {"percentDone":88};
stats['da_DK'] = {"percentDone":98};
stats['de_DE'] = {"percentDone":98};
stats['et_EE'] = {"percentDone":58};
stats['en_GB'] = {"percentDone":100};
stats['en_US'] = {"percentDone":100};
stats['es_ES'] = {"percentDone":97};
stats['eo'] = {"percentDone":34};
stats['eo'] = {"percentDone":33};
stats['fi_FI'] = {"percentDone":97};
stats['fr_FR'] = {"percentDone":95};
stats['fr_FR'] = {"percentDone":94};
stats['gl_ES'] = {"percentDone":39};
stats['id_ID'] = {"percentDone":96};
stats['id_ID'] = {"percentDone":95};
stats['it_IT'] = {"percentDone":97};
stats['hu_HU'] = {"percentDone":91};
stats['nl_BE'] = {"percentDone":95};
stats['nl_NL'] = {"percentDone":98};
stats['hu_HU'] = {"percentDone":90};
stats['nl_BE'] = {"percentDone":94};
stats['nl_NL'] = {"percentDone":97};
stats['nb_NO'] = {"percentDone":78};
stats['fa'] = {"percentDone":74};
stats['fa'] = {"percentDone":73};
stats['pl_PL'] = {"percentDone":97};
stats['pt_BR'] = {"percentDone":97};
stats['pt_PT'] = {"percentDone":97};
stats['ro'] = {"percentDone":68};
stats['sl_SI'] = {"percentDone":99};
stats['sl_SI'] = {"percentDone":98};
stats['sv'] = {"percentDone":63};
stats['th_TH'] = {"percentDone":47};
stats['th_TH'] = {"percentDone":46};
stats['vi'] = {"percentDone":75};
stats['tr_TR'] = {"percentDone":97};
stats['uk_UA'] = {"percentDone":97};
stats['el_GR'] = {"percentDone":85};
stats['el_GR'] = {"percentDone":84};
stats['ru_RU'] = {"percentDone":97};
stats['sr_RS'] = {"percentDone":73};
stats['zh_CN'] = {"percentDone":97};
stats['zh_TW'] = {"percentDone":95};
stats['ja_JP'] = {"percentDone":98};
stats['ko'] = {"percentDone":97};
stats['ko'] = {"percentDone":99};
module.exports = { locales: locales, stats: stats };

File diff suppressed because one or more lines are too long

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