1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-26 23:38:08 +02:00

Compare commits

..

36 Commits

Author SHA1 Message Date
Laurent Cozic
a1652d57d7 Server: Add support for user max item size 2021-05-17 17:54:13 +02:00
Laurent Cozic
ec7f0f479a Server: Improved log table too and made it sortable 2021-05-17 17:29:21 +02:00
Laurent Cozic
7f05420fda Server: Improved Items table and added item size to it 2021-05-17 17:02:15 +02:00
Laurent Cozic
a3f8cd4850 Merge branch 'release-2.0' into dev 2021-05-16 19:40:08 +02:00
Laurent Cozic
01ccf5170a Tools: Changelog for Android version 2021-05-16 19:38:36 +02:00
Laurent Cozic
6ddb69e1ea Server: Fixed bug when unsharing a notebook that has no recipients 2021-05-16 18:55:07 +02:00
Laurent Cozic
b01f82bb33 Android release v2.0.1 2021-05-16 17:54:13 +02:00
Laurent Cozic
b6c9edba21 Tools: Make sure branch has been pushed before releasing Android version 2021-05-16 17:50:50 +02:00
Laurent Cozic
f7d164be6e Desktop: Allow unsharing a note 2021-05-16 17:28:49 +02:00
Laurent Cozic
6f2f24171d Desktop: Add Share Notebook menu item 2021-05-16 15:21:55 +02:00
Laurent Cozic
12cc64008b typo 2021-05-16 12:49:05 +02:00
Laurent Cozic
b9955f58d3 Server: Refactor ShareType 2021-05-16 12:46:58 +02:00
Laurent Cozic
489995daef Server: Fixed deleting a note that has been shared 2021-05-16 12:42:58 +02:00
Laurent Cozic
e156ee1b58 Server: Generate only one share link per note 2021-05-16 12:33:36 +02:00
Laurent Cozic
a24b0091ad Server: Go back to home page when there is an error and user is logged in 2021-05-16 12:19:18 +02:00
Laurent Cozic
2655b6deee Merge branch 'dev' of github.com:laurent22/joplin into dev 2021-05-16 11:48:15 +02:00
Laurent Cozic
45c40f7395 Server: Fixed log page 2021-05-16 11:46:16 +02:00
Woosuk Park
ecb0eee355 All: Translation: Update ko.po (#4976)
* Some Translated. and some modified

* ko.po Update
2021-05-16 02:39:43 -04:00
suixinio
4916f4cc92 All: Translation: Update zh_CN.po (#4969)
This PR updates zh_CN.po, with just a minor translation fix.
2021-05-15 15:58:48 -04:00
Laurent Cozic
15fe119256 Desktop: Made sync more reliable by making it skip items that time out, and improved sync status screen 2021-05-15 20:56:49 +02:00
Laurent Cozic
0b46880a00 Desktop: Fixes #4926: Fixed issue with empty panels being created by plugins 2021-05-15 17:30:56 +02:00
Laurent Cozic
deaa731983 Update translations 2021-05-15 16:15:55 +02:00
Laurent Cozic
d061bb1a4f typo 2021-05-15 16:06:24 +02:00
Laurent Cozic
03db0c5486 Desktop: Resolves #4462: Improved usability when plugin repository cannot be connected to 2021-05-15 16:04:10 +02:00
Laurent Cozic
bb275e671d Tools: Allow running the test units with Postgres 2021-05-15 15:13:08 +02:00
Laurent Cozic
2d0580ff71 Server: Fixed /items page when using Postgres 2021-05-15 15:10:40 +02:00
Laurent Cozic
2331d3487b Desktop, Cli: Resolves #4968: Import SVG as images when importing ENEX files 2021-05-15 13:42:57 +02:00
Laurent Cozic
f1380fd51d Server: Fixes #4540: Make sure temp files are deleted after upload is done 2021-05-15 12:13:46 +02:00
Laurent Cozic
d462dab8eb Update French translation 2021-05-15 11:36:52 +02:00
Laurent Cozic
cf37b74d9a Merge branch 'dev' of github.com:laurent22/joplin into dev 2021-05-15 11:14:40 +02:00
Laurent Cozic
aec3ea9c0c Desktop, Cli: Fixes #4965: Improved importing Evernote notes that contain codeblocks 2021-05-15 11:12:11 +02:00
Helmut K. C. Tessarek
1f5aa70acd Update translations (for new server/client code) 2021-05-14 13:27:17 -04:00
Harris Arvanitis
416637ce83 All: Translation: Update el_GR.po (#4961) 2021-05-14 13:23:13 -04:00
Gen Neko
99c4b0bc01 All: Translation: Update ja_JP.po (#4960) 2021-05-14 13:22:35 -04:00
Laurent Cozic
74d8fec98a Desktop release v2.0.1 2021-05-14 17:17:40 +02:00
Laurent Cozic
321a58c356 Prepare for v2 2021-05-14 17:17:02 +02:00
170 changed files with 20309 additions and 13727 deletions

View File

@@ -578,6 +578,9 @@ packages/app-desktop/gui/ResizableLayout/utils/persist.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/removeItem.d.ts
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js.map
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.d.ts
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.js
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.js.map
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.d.ts
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js.map
@@ -677,6 +680,9 @@ 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/StyledLink.d.ts
packages/app-desktop/gui/style/StyledLink.js
packages/app-desktop/gui/style/StyledLink.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
@@ -1100,6 +1106,9 @@ packages/lib/services/UndoRedoService.js.map
packages/lib/services/WhenClause.d.ts
packages/lib/services/WhenClause.js
packages/lib/services/WhenClause.js.map
packages/lib/services/WhenClause.test.d.ts
packages/lib/services/WhenClause.test.js
packages/lib/services/WhenClause.test.js.map
packages/lib/services/commands/MenuUtils.d.ts
packages/lib/services/commands/MenuUtils.js
packages/lib/services/commands/MenuUtils.js.map
@@ -1568,6 +1577,9 @@ packages/tools/generate-database-types.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map
packages/tools/release-android.d.ts
packages/tools/release-android.js
packages/tools/release-android.js.map
packages/tools/release-cli.d.ts
packages/tools/release-cli.js
packages/tools/release-cli.js.map

View File

@@ -76,7 +76,7 @@ module.exports = {
// Warn only for now because fixing everything would take too much
// refactoring, but new code should try to stick to it.
'complexity': ['warn', { max: 10 }],
// 'complexity': ['warn', { max: 10 }],
// Checks rules of Hooks
'react-hooks/rules-of-hooks': 'error',

12
.gitignore vendored
View File

@@ -564,6 +564,9 @@ packages/app-desktop/gui/ResizableLayout/utils/persist.test.js.map
packages/app-desktop/gui/ResizableLayout/utils/removeItem.d.ts
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js
packages/app-desktop/gui/ResizableLayout/utils/removeItem.js.map
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.d.ts
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.js
packages/app-desktop/gui/ResizableLayout/utils/removeKeylessItems.js.map
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.d.ts
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js
packages/app-desktop/gui/ResizableLayout/utils/setLayoutItemProps.js.map
@@ -663,6 +666,9 @@ 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/StyledLink.d.ts
packages/app-desktop/gui/style/StyledLink.js
packages/app-desktop/gui/style/StyledLink.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
@@ -1086,6 +1092,9 @@ packages/lib/services/UndoRedoService.js.map
packages/lib/services/WhenClause.d.ts
packages/lib/services/WhenClause.js
packages/lib/services/WhenClause.js.map
packages/lib/services/WhenClause.test.d.ts
packages/lib/services/WhenClause.test.js
packages/lib/services/WhenClause.test.js.map
packages/lib/services/commands/MenuUtils.d.ts
packages/lib/services/commands/MenuUtils.js
packages/lib/services/commands/MenuUtils.js.map
@@ -1554,6 +1563,9 @@ packages/tools/generate-database-types.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map
packages/tools/release-android.d.ts
packages/tools/release-android.js
packages/tools/release-android.js.map
packages/tools/release-cli.d.ts
packages/tools/release-cli.js
packages/tools/release-cli.js.map

View File

@@ -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) | 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) | 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) | 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/arableague.png) | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 96%
![](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 | 30%
![](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) | 75%
![](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) | | 58%
![](https://joplinapp.org/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 83%
![](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) | 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) | 86%
![](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/de.png) | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
![](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) | | 57%
![](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/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) | 94%
![](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 | 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) | 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) | 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) | 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) | 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) | | 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) | 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) | 99%
![](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 | 94%
![](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 | 99%
![](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) | 38%
![](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) | 93%
![](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) | 94%
![](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) | 88%
![](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) | | 92%
![](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) | 95%
![](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) | 76%
![](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) | 71%
![](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) | 94%
![](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) | 94%
![](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) | 94%
![](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) | 66%
![](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) | 96%
![](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) | 61%
![](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) | | 45%
![](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) | | 73%
![](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) | 94%
![](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) | 94%
![](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) | 97%
![](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) | 94%
![](https://joplinapp.org/images/flags/country-4x3/rs.png) | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 71%
![](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) | 94%
![](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) | 92%
![](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) | 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) | 96%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributors

View File

@@ -5,7 +5,7 @@
"author": "Laurent Cozic",
"private": true,
"scripts": {
"test": "jest --config=jest.config.js --bail --forceExit",
"test": "jest --verbose=false --config=jest.config.js --bail --forceExit",
"test-one": "jest --verbose=false --config=jest.config.js --bail --forceExit",
"test-ci": "jest --config=jest.config.js --forceExit",
"build": "gulp build",

View File

@@ -1,14 +1,16 @@
For example, consider a web page like this:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
```
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
<script src="page-scripts/page-script.js"></script>
</body>
</html>
<body>
<script src="page-scripts/page-script.js"></script>
</body>
</html>
```
The script "page-script.js" does this:

View File

@@ -1,7 +1,9 @@
Subshell:
(
set -e
false
echo Unreachable
) && echo Great success
```
(
set -e
false
echo Unreachable
) && echo Great success
```

View File

@@ -1 +1 @@
jq -r '.[]|[.index, .name, .section, .award, .industry]|join("\t")' raw.json |pbcopy
`jq -r '.[]|[.index, .name, .section, .award, .industry]|join("\t")' raw.json |pbcopy`

View File

@@ -0,0 +1 @@
<div><div>code block:</div><div><br/></div><div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; font-size: 12px; color: rgb(51, 51, 51); border-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.15);-en-codeblock:true;"><div>public static void main(String[] args) {</div><div><span>&nbsp; &nbsp; System.out.println('Hello World');</span><br/></div><div>}</div></div><div><br/></div><div>end of code block</div></div>

View File

@@ -0,0 +1,9 @@
code block:
```
public static void main(String[] args) {
    System.out.println('Hello World');
}
```
end of code block

View File

@@ -13,8 +13,13 @@ import { PluginItem } from './PluginBox';
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
import Setting from '@joplin/lib/models/Setting';
import useOnInstallHandler, { OnPluginSettingChangeEvent } from './useOnInstallHandler';
import Logger from '@joplin/lib/Logger';
import StyledMessage from '../../../style/StyledMessage';
import StyledLink from '../../../style/StyledLink';
const { space } = require('styled-system');
const logger = Logger.create('PluginState');
const maxWidth: number = 320;
const Root = styled.div`
@@ -32,6 +37,11 @@ const ToolsButton = styled(Button)`
margin-right: 6px;
`;
const RepoApiErrorMessage = styled(StyledMessage)`
max-width: ${props => props.maxWidth}px;
margin-bottom: 10px;
`;
interface Props {
value: any;
themeId: number;
@@ -84,6 +94,8 @@ export default function(props: Props) {
const [manifestsLoaded, setManifestsLoaded] = useState<boolean>(false);
const [updatingPluginsIds, setUpdatingPluginIds] = useState<Record<string, boolean>>({});
const [canBeUpdatedPluginIds, setCanBeUpdatedPluginIds] = useState<Record<string, boolean>>({});
const [repoApiError, setRepoApiError] = useState<Error>(null);
const [fetchManifestTime, setFetchManifestTime] = useState<number>(Date.now());
const pluginService = PluginService.instance();
@@ -96,9 +108,25 @@ export default function(props: Props) {
useEffect(() => {
let cancelled = false;
async function fetchManifests() {
await repoApi().loadManifests();
setManifestsLoaded(false);
setRepoApiError(null);
let loadError: Error = null;
try {
await repoApi().loadManifests();
} catch (error) {
logger.error(error);
loadError = error;
}
if (cancelled) return;
setManifestsLoaded(true);
if (loadError) {
setManifestsLoaded(false);
setRepoApiError(loadError);
} else {
setManifestsLoaded(true);
}
}
void fetchManifests();
@@ -106,7 +134,7 @@ export default function(props: Props) {
return () => {
cancelled = true;
};
}, []);
}, [fetchManifestTime]);
useEffect(() => {
if (!manifestsLoaded) return () => {};
@@ -252,7 +280,7 @@ export default function(props: Props) {
function renderSearchArea() {
return (
<div style={{ marginBottom: 20 }}>
<div style={{ marginBottom: 0 }}>
<SearchPlugins
disabled={!manifestsLoaded}
maxWidth={maxWidth}
@@ -268,11 +296,18 @@ export default function(props: Props) {
);
}
function renderRepoApiError() {
if (!repoApiError) return null;
return <RepoApiErrorMessage maxWidth={maxWidth} type="error">{_('Could not connect to plugin repository')} - <StyledLink href="#" onClick={() => { setFetchManifestTime(Date.now()); }}>{_('Try again')}</StyledLink></RepoApiErrorMessage>;
}
function renderBottomArea() {
if (searchQuery) return null;
return (
<div>
{renderRepoApiError()}
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
<ToolsButton tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
<div style={{ display: 'flex', flex: 1 }}>

View File

@@ -101,7 +101,7 @@ export default function(props: Props) {
onChange={onChange}
onSearchButtonClick={onSearchButtonClick}
searchStarted={searchStarted}
placeholder={_('Search for plugins...')}
placeholder={props.disabled ? _('Please wait...') : _('Search for plugins...')}
disabled={props.disabled}
/>
</div>

View File

@@ -34,6 +34,7 @@ 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';
import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems';
const { connect } = require('react-redux');
const { PromptDialog } = require('../PromptDialog.min.js');
@@ -234,6 +235,14 @@ class MainScreenComponent extends React.Component<Props, State> {
try {
output = loadLayout(Object.keys(userLayout).length ? userLayout : null, defaultLayout, rootLayoutSize);
// For unclear reasons, layout items sometimes end up witout a key.
// In that case, we can't do anything with them, so remove them
// here. It could be due to the deprecated plugin API, which allowed
// creating panel without a key, although in this case it should
// have been set automatically.
// https://github.com/laurent22/joplin/issues/4926
output = removeKeylessItems(output);
if (!findItemByKey(output, 'sideBar') || !findItemByKey(output, 'noteList') || !findItemByKey(output, 'editor')) {
throw new Error('"sideBar", "noteList" and "editor" must be present in the layout');
}

View File

@@ -18,6 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
},
});
},
enabledCondition: 'folderIsShareRootAndOwnedByUser || !folderIsShared',
enabledCondition: 'joplinServerConnected && (folderIsShareRootAndOwnedByUser || !folderIsShared)',
};
};

View File

@@ -18,5 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
},
});
},
enabledCondition: 'joplinServerConnected && someNotesSelected',
};
};

View File

@@ -695,11 +695,18 @@ function useMenu(props: Props) {
},
],
},
folder: {
label: _('Note&book'),
submenu: [
menuItemDic.showShareFolderDialog,
],
},
note: {
label: _('&Note'),
submenu: [
menuItemDic.toggleExternalEditing,
menuItemDic.setTags,
menuItemDic.showShareNoteDialog,
separator(),
menuItemDic.showNoteContentProperties,
],
@@ -818,6 +825,7 @@ function useMenu(props: Props) {
rootMenus.edit,
rootMenus.view,
rootMenus.go,
rootMenus.folder,
rootMenus.note,
rootMenus.tools,
rootMenus.help,

View File

@@ -110,6 +110,7 @@ function NoteListItem(props: NoteListItemProps, ref: any) {
let listItemTitleStyle = Object.assign({}, props.style.listItemTitle);
listItemTitleStyle.paddingLeft = !item.is_todo ? hPadding : 4;
if (item.is_shared) listItemTitleStyle.color = theme.colorWarn;
if (item.is_todo && !!item.todo_completed) listItemTitleStyle = Object.assign(listItemTitleStyle, props.style.listItemTitleCompleted);
const displayTitle = Note.displayTitle(item);

View File

@@ -0,0 +1,30 @@
import produce from 'immer';
import iterateItems from './iterateItems';
import { LayoutItem } from './types';
import validateLayout from './validateLayout';
interface ItemToRemove {
parent: LayoutItem;
index: number;
}
export default function(layout: LayoutItem): LayoutItem {
const itemsToRemove: ItemToRemove[] = [];
const output = produce(layout, (layoutDraft: LayoutItem) => {
iterateItems(layoutDraft, (itemIndex: number, item: LayoutItem, parent: LayoutItem) => {
if (!item.key) itemsToRemove.push({ parent, index: itemIndex });
return true;
});
itemsToRemove.sort((a: ItemToRemove, b: ItemToRemove) => {
return a.index > b.index ? -1 : +1;
});
for (const item of itemsToRemove) {
item.parent.children.splice(item.index, 1);
}
});
return output !== layout ? validateLayout(output) : layout;
}

View File

@@ -10,19 +10,21 @@ import { reg } from '@joplin/lib/registry';
import Dialog from './Dialog';
import DialogTitle from './DialogTitle';
import ShareService from '@joplin/lib/services/share/ShareService';
import { StateShare } from '@joplin/lib/services/share/reducer';
import { NoteEntity } from '@joplin/lib/services/database/types';
import Button from './Button/Button';
import { connect } from 'react-redux';
import { AppState } from '../app';
const { clipboard } = require('electron');
interface ShareNoteDialogProps {
interface Props {
themeId: number;
noteIds: Array<string>;
onClose: Function;
shares: StateShare[];
}
interface SharesMap {
[key: string]: any;
}
function styles_(props: ShareNoteDialogProps) {
function styles_(props: Props) {
return buildStyle('ShareNoteDialog', props.themeId, (theme: any) => {
return {
noteList: {
@@ -60,17 +62,21 @@ function styles_(props: ShareNoteDialogProps) {
});
}
export default function ShareNoteDialog(props: ShareNoteDialogProps) {
export function ShareNoteDialog(props: Props) {
console.info('Render ShareNoteDialog');
const [notes, setNotes] = useState<any[]>([]);
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [sharesState, setSharesState] = useState<string>('unknown');
const [shares, setShares] = useState<SharesMap>({});
// const [shares, setShares] = useState<SharesMap>({});
const noteCount = notes.length;
const theme = themeStyle(props.themeId);
const styles = styles_(props);
useEffect(() => {
void ShareService.instance().refreshShares();
}, []);
useEffect(() => {
async function fetchNotes() {
const result = [];
@@ -87,9 +93,9 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
props.onClose();
};
const copyLinksToClipboard = (shares: SharesMap) => {
const copyLinksToClipboard = (shares: StateShare[]) => {
const links = [];
for (const n in shares) links.push(ShareService.instance().shareUrl(shares[n]));
for (const share of shares) links.push(ShareService.instance().shareUrl(share));
clipboard.writeText(links.join('\n'));
};
@@ -109,15 +115,13 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
setSharesState('creating');
const newShares = Object.assign({}, shares);
const newShares: StateShare[] = [];
for (const note of notes) {
const share = await service.shareNote(note.id);
newShares[note.id] = share;
newShares.push(share);
}
setShares(newShares);
setSharesState('synchronizing');
await reg.waitForSyncFinishedThenSync();
setSharesState('creating');
@@ -125,6 +129,8 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
copyLinksToClipboard(newShares);
setSharesState('created');
await ShareService.instance().refreshShares();
} catch (error) {
if (error.code === 404 && !hasSynced) {
reg.logger().info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
@@ -142,34 +148,53 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
}
};
const removeNoteButton_click = (event: any) => {
const newNotes = [];
for (let i = 0; i < notes.length; i++) {
const n = notes[i];
if (n.id === event.noteId) continue;
newNotes.push(n);
}
setNotes(newNotes);
// const removeNoteButton_click = (event: any) => {
// const newNotes = [];
// for (let i = 0; i < notes.length; i++) {
// const n = notes[i];
// if (n.id === event.noteId) continue;
// newNotes.push(n);
// }
// setNotes(newNotes);
// };
const unshareNoteButton_click = async (event: any) => {
await ShareService.instance().unshareNote(event.noteId);
await ShareService.instance().refreshShares();
};
const renderNote = (note: any) => {
const removeButton = notes.length <= 1 ? null : (
<button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
<i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
</button>
const renderNote = (note: NoteEntity) => {
const unshareButton = !props.shares.find(s => s.note_id === note.id) ? null : (
<Button tooltip={_('Unshare note')} iconName="fas fa-share-alt" onClick={() => unshareNoteButton_click({ noteId: note.id })}/>
);
// const removeButton = notes.length <= 1 ? null : (
// <Button iconName="fa fa-times" onClick={() => removeNoteButton_click({ noteId: note.id })}/>
// );
// const unshareButton = !shares[note.id] ? null : (
// <button onClick={() => unshareNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fas fa-share-alt'}></i>
// </button>
// );
// const removeButton = notes.length <= 1 ? null : (
// <button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
// <i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
// </button>
// );
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{removeButton}
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
</div>
);
};
const renderNoteList = (notes: any) => {
const noteComps = [];
for (const noteId of Object.keys(notes)) {
noteComps.push(renderNote(notes[noteId]));
for (const note of notes) {
noteComps.push(renderNote(note));
}
return <div style={styles.noteList}>{noteComps}</div>;
};
@@ -194,7 +219,12 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
<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()}
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
<DialogButtonRow
themeId={props.themeId}
onClick={buttonRow_click}
okButtonShow={false}
cancelButtonLabel={_('Close')}
/>
</div>
);
}
@@ -203,3 +233,11 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
<Dialog renderContent={renderContent}/>
);
}
const mapStateToProps = (state: AppState) => {
return {
shares: state.shareService.shares.filter(s => !!s.note_id),
};
};
export default connect(mapStateToProps)(ShareNoteDialog as any);

View File

@@ -69,7 +69,6 @@ function listItemTextColor(props: any) {
export const StyledListItemAnchor = styled.a`
font-size: ${(props: any) => Math.round(props.theme.fontSize * 1.0833333)}px;
// font-weight: 500;
text-decoration: none;
color: ${(props: any) => listItemTextColor(props)};
cursor: default;

View File

@@ -87,12 +87,15 @@ function StatusScreen(props: Props) {
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
let currentListKey = '';
let listItems: any[] = [];
for (const n in section.body) {
if (!section.body.hasOwnProperty(n)) continue;
const item = section.body[n];
let text = '';
let retryLink = null;
let itemType = null;
if (typeof item === 'object') {
if (item.canRetry) {
const onClick = async () => {
@@ -107,18 +110,40 @@ function StatusScreen(props: Props) {
);
}
text = item.text;
itemType = item.type;
} else {
text = item;
}
if (itemType === 'openList') {
currentListKey = item.key;
continue;
}
if (itemType === 'closeList') {
itemsHtml.push(<ul key={currentListKey}>{listItems}</ul>);
currentListKey = '';
listItems = [];
continue;
}
if (!text) text = '\xa0';
itemsHtml.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
</div>
);
if (currentListKey) {
listItems.push(
<li style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
</li>
);
} else {
itemsHtml.push(
<div style={theme.textStyle} key={`item_${n}`}>
<span>{text}</span>
{retryLink}
</div>
);
}
}
if (section.canRetryAll) {

View File

@@ -45,5 +45,7 @@ export default function() {
'editor.swapLineUp',
'editor.swapLineDown',
'toggleSafeMode',
'showShareNoteDialog',
'showShareFolderDialog',
];
}

View File

@@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledLink = styled.a`
font-size: ${props => props.theme.fontSize}px;
color: ${props => props.theme.urlColor};
font-family: ${props => props.theme.fontFamily};
`;
export default StyledLink;

View File

@@ -7,6 +7,7 @@ const StyledMessage = styled.div`
color: ${props => props.theme.color};
font-family: ${props => props.theme.fontFamily};
padding: ${props => props.type === 'error' ? props.theme.mainPadding : '0'}px;
word-break: break-all;
`;
export default StyledMessage;

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.8.5",
"version": "2.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

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

View File

@@ -35,6 +35,7 @@ if [ "$1" == "1" ]; then
echo 'mkbook "other"' >> "$CMD_FILE"
echo 'use "shared"' >> "$CMD_FILE"
echo 'mknote "note 1"' >> "$CMD_FILE"
echo 'mknote "note 2"' >> "$CMD_FILE"
fi
cd "$ROOT_DIR/packages/app-cli"

View File

@@ -141,8 +141,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097631
versionName "2.0.0"
versionCode 2097632
versionName "2.0.1"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -28,6 +28,22 @@ interface RemoteItem {
type_?: number;
}
function isCannotSyncError(error: any): boolean {
if (!error) return false;
if (['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) return true;
// If the request times out we give up too because sometimes it's due to the
// file being large or some other connection issues, and we don't want that
// file to block the sync process. The user can choose to retry later on.
//
// message: "network timeout at: .....
// name: "FetchError"
// type: "request-timeout"
if (error.type === 'request-timeout' || error.message.includes('network timeout')) return true;
return false;
}
export default class Synchronizer {
private db_: any;
@@ -514,7 +530,7 @@ export default class Synchronizer {
await this.apiCall('put', remoteContentPath, null, { path: localResourceContentPath, source: 'file', shareId: local.share_id });
} catch (error) {
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
if (isCannotSyncError(error)) {
await handleCannotSyncItem(ItemClass, syncTargetId, local, error.message);
action = null;
} else {

View File

@@ -31,6 +31,7 @@ interface Section {
interface ParserStateTag {
name: string;
visible: boolean;
isCodeBlock: boolean;
}
interface ParserStateList {
@@ -443,6 +444,20 @@ function isInvisibleBlock(context: any, attributes: any) {
return display && display.indexOf('none') === 0;
}
function trimBlockOpenAndClose(lines: string[]): string[] {
const output = lines.slice();
while (output.length && [BLOCK_OPEN, BLOCK_CLOSE, ''].includes(output[0])) {
output.splice(0, 1);
}
while (output.length && [BLOCK_OPEN, BLOCK_CLOSE, ''].includes(output[output.length - 1])) {
output.pop();
}
return output;
}
function isSpanWithStyle(attributes: any) {
if (attributes != undefined) {
if ('style' in attributes) {
@@ -484,6 +499,16 @@ function displaySaxWarning(context: any, message: string) {
console.warn(line.join(': '));
}
function isCodeBlock(context: any, nodeName: string, attributes: any) {
if (nodeName === 'code') return true;
if (attributes && attributes.style) {
const enCodeBlock = cssValue(context, attributes.style, '-en-codeblock');
if (enCodeBlock && enCodeBlock.toLowerCase() === 'true') return true;
}
return false;
}
// function removeSectionParent(section:Section | string) {
// if (typeof section === 'string') return section;
@@ -575,11 +600,13 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[]): Promise<Ene
const nodeAttributes = attributeToLowerCase(node);
const n = node.name.toLowerCase();
const isVisible = !isInvisibleBlock(this, nodeAttributes);
state.tags.push({
const tagInfo: ParserStateTag = {
name: n,
visible: isVisible,
});
isCodeBlock: isCodeBlock(this, n, nodeAttributes),
};
state.tags.push(tagInfo);
const currentList = state.lists && state.lists.length ? state.lists[state.lists.length - 1] : null;
@@ -673,6 +700,25 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[]): Promise<Ene
lines: [],
parent: section,
};
section.lines.push(newSection);
section = newSection;
} else if (tagInfo.isCodeBlock) {
// state.inPre = false;
// const previousIsPre = state.tags.length ? state.tags[state.tags.length - 1].name === 'pre' : false;
// if (previousIsPre) {
// section.lines.pop();
// }
state.inCode.push(true);
state.currentCode = '';
const newSection: Section = {
type: SectionType.Code,
lines: [],
parent: section,
};
section.lines.push(newSection);
section = newSection;
} else if (isBlockTag(n)) {
@@ -750,18 +796,6 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[]): Promise<Ene
} else if (n == 'blockquote') {
section.lines.push(BLOCK_OPEN);
state.inQuote = true;
} else if (n === 'code') {
state.inCode.push(true);
state.currentCode = '';
const newSection: Section = {
type: SectionType.Code,
lines: [],
parent: section,
};
section.lines.push(newSection);
section = newSection;
} else if (n === 'pre') {
section.lines.push(BLOCK_OPEN);
state.inPre = true;
@@ -871,6 +905,28 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[]): Promise<Ene
// End of note
} else if (!poppedTag.visible) {
if (section && section.parent) section = section.parent;
} else if (poppedTag.isCodeBlock) {
state.inCode.pop();
if (!state.inCode.length) {
// When a codeblock is wrapped in <pre><code>, it will have
// extra empty lines added by the "pre" logic, but since we
// are in a codeblock we should actually trim those.
const codeLines = trimBlockOpenAndClose(processMdArrayNewLines(section.lines).split('\n'));
section.lines = [];
if (codeLines.length > 1) {
section.lines.push('\n\n```\n');
for (let i = 0; i < codeLines.length; i++) {
if (i > 0) section.lines.push('\n');
section.lines.push(codeLines[i]);
}
section.lines.push('\n```\n\n');
} else {
section.lines.push(`\`${markdownUtils.escapeInlineCode(codeLines.join(''))}\``);
}
if (section && section.parent) section = section.parent;
}
} else if (isNewLineOnlyEndTag(n)) {
section.lines.push(BLOCK_CLOSE);
} else if (n == 'td' || n == 'th') {
@@ -897,23 +953,6 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[]): Promise<Ene
} else if (n == 'blockquote') {
section.lines.push(BLOCK_OPEN);
state.inQuote = false;
} else if (n === 'code') {
state.inCode.pop();
if (!state.inCode.length) {
const codeLines = processMdArrayNewLines(section.lines).split('\n');
section.lines = [];
if (codeLines.length > 1) {
for (let i = 0; i < codeLines.length; i++) {
if (i > 0) section.lines.push('\n');
section.lines.push(`\t${codeLines[i]}`);
}
} else {
section.lines.push(`\`${codeLines.join('')}\``);
}
if (section && section.parent) section = section.parent;
}
} else if (n === 'pre') {
state.inPre = false;
section.lines.push(BLOCK_CLOSE);

View File

@@ -578,7 +578,7 @@ function localesFromLanguageCode(languageCode: string, locales: string[]): strin
});
}
function _(s: string, ...args: any[]) {
function _(s: string, ...args: any[]): string {
const strings = localeStrings(currentLocale_);
let result = strings[s];
if (result === '' || result === undefined) result = s;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because 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":98};
stats['eu'] = {"percentDone":31};
stats['bs_BA'] = {"percentDone":76};
stats['bg_BG'] = {"percentDone":59};
stats['ca'] = {"percentDone":84};
stats['hr_HR'] = {"percentDone":99};
stats['cs_CZ'] = {"percentDone":88};
stats['da_DK'] = {"percentDone":98};
stats['de_DE'] = {"percentDone":98};
stats['et_EE'] = {"percentDone":58};
stats['ar'] = {"percentDone":96};
stats['eu'] = {"percentDone":30};
stats['bs_BA'] = {"percentDone":75};
stats['bg_BG'] = {"percentDone":58};
stats['ca'] = {"percentDone":83};
stats['hr_HR'] = {"percentDone":96};
stats['cs_CZ'] = {"percentDone":86};
stats['da_DK'] = {"percentDone":96};
stats['de_DE'] = {"percentDone":95};
stats['et_EE'] = {"percentDone":57};
stats['en_GB'] = {"percentDone":100};
stats['en_US'] = {"percentDone":100};
stats['es_ES'] = {"percentDone":97};
stats['es_ES'] = {"percentDone":94};
stats['eo'] = {"percentDone":33};
stats['fi_FI'] = {"percentDone":97};
stats['fr_FR'] = {"percentDone":94};
stats['gl_ES'] = {"percentDone":39};
stats['id_ID'] = {"percentDone":95};
stats['it_IT'] = {"percentDone":97};
stats['hu_HU'] = {"percentDone":90};
stats['nl_BE'] = {"percentDone":94};
stats['nl_NL'] = {"percentDone":97};
stats['nb_NO'] = {"percentDone":78};
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":98};
stats['sv'] = {"percentDone":63};
stats['th_TH'] = {"percentDone":46};
stats['vi'] = {"percentDone":75};
stats['tr_TR'] = {"percentDone":97};
stats['uk_UA'] = {"percentDone":97};
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":99};
stats['fi_FI'] = {"percentDone":94};
stats['fr_FR'] = {"percentDone":99};
stats['gl_ES'] = {"percentDone":38};
stats['id_ID'] = {"percentDone":93};
stats['it_IT'] = {"percentDone":94};
stats['hu_HU'] = {"percentDone":88};
stats['nl_BE'] = {"percentDone":92};
stats['nl_NL'] = {"percentDone":95};
stats['nb_NO'] = {"percentDone":76};
stats['fa'] = {"percentDone":71};
stats['pl_PL'] = {"percentDone":94};
stats['pt_BR'] = {"percentDone":94};
stats['pt_PT'] = {"percentDone":94};
stats['ro'] = {"percentDone":66};
stats['sl_SI'] = {"percentDone":96};
stats['sv'] = {"percentDone":61};
stats['th_TH'] = {"percentDone":45};
stats['vi'] = {"percentDone":73};
stats['tr_TR'] = {"percentDone":94};
stats['uk_UA'] = {"percentDone":94};
stats['el_GR'] = {"percentDone":97};
stats['ru_RU'] = {"percentDone":94};
stats['sr_RS'] = {"percentDone":71};
stats['zh_CN'] = {"percentDone":94};
stats['zh_TW'] = {"percentDone":92};
stats['ja_JP'] = {"percentDone":97};
stats['ko'] = {"percentDone":96};
module.exports = { locales: locales, stats: stats };

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -41,6 +41,11 @@ const markdownUtils = {
return text;
},
escapeInlineCode(text: string): string {
// https://github.com/github/markup/issues/363#issuecomment-55499909
return text.replace(/`/g, '``');
},
unescapeLinkUrl(url: string) {
url = url.replace(/%28/g, '(');
url = url.replace(/%29/g, ')');

View File

@@ -756,6 +756,10 @@ export default class BaseItem extends BaseModel {
return this.db().transactionExecBatch(queries);
}
public static async saveSyncEnabled(itemType: ModelType, itemId: string) {
await this.db().exec('DELETE FROM sync_items WHERE item_type = ? AND item_id = ?', [itemType, itemId]);
}
// When an item is deleted, its associated sync_items data is not immediately deleted for
// performance reason. So this function is used to look for these remaining sync_items and
// delete them.

View File

@@ -347,7 +347,8 @@ export default class Folder extends BaseItem {
public static async updateResourceShareIds() {
// Find all resources where share_id is different from parent note
// share_id. Then update share_id on all these resources. Essentially it
// makes it match the resource share_id to the note share_id.
// makes it match the resource share_id to the note share_id. At the
// same time we also process the is_shared property.
const rows = await this.db().selectAll(`
SELECT r.id, n.share_id, n.is_shared
FROM note_resources nr

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "1.8.2",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -12,6 +12,7 @@ const imageMimeTypes = [
'image/png',
'image/prs.btif',
'image/prs.pti',
'image/svg+xml',
'image/t38',
'image/tiff',
'image/tiff-fx',

View File

@@ -10,6 +10,36 @@ import Resource from '../models/Resource';
import { _ } from '../locale';
const { toTitleCase } = require('../string-utils.js');
enum CanRetryType {
E2EE = 'e2ee',
ResourceDownload = 'resourceDownload',
ItemSync = 'itemSync',
}
enum ReportItemType {
OpenList = 'openList',
CloseList = 'closeList',
}
type RerportItemOrString = ReportItem | string;
interface ReportSection {
title: string;
body: RerportItemOrString[];
name?: string;
canRetryAll?: boolean;
retryAllHandler?: ()=> void;
}
interface ReportItem {
type?: ReportItemType;
key?: string;
text?: string;
canRetry?: boolean;
canRetryType?: CanRetryType;
retryHandler?: ()=> void;
}
export default class ReportService {
csvEscapeCell(cell: string) {
cell = this.csvValueToString(cell);
@@ -110,10 +140,10 @@ export default class ReportService {
return output;
}
async status(syncTarget: number) {
async status(syncTarget: number): Promise<ReportSection[]> {
const r = await this.syncStatus(syncTarget);
const sections = [];
let section: any = null;
const sections: ReportSection[] = [];
let section: ReportSection = null;
const disabledItems = await BaseItem.syncDisabledItems(syncTarget);
@@ -122,17 +152,29 @@ export default class ReportService {
section.body.push(_('These items will remain on the device but will not be uploaded to the sync target. In order to find these items, either search for the title or the ID (which is displayed in brackets above).'));
section.body.push('');
section.body.push({ type: ReportItemType.OpenList, key: 'disabledSyncItems' });
for (let i = 0; i < disabledItems.length; i++) {
const row = disabledItems[i];
let msg: string = '';
if (row.location === BaseItem.SYNC_ITEM_LOCATION_LOCAL) {
section.body.push(_('%s (%s) could not be uploaded: %s', row.item.title, row.item.id, row.syncInfo.sync_disabled_reason));
msg = _('%s (%s) could not be uploaded: %s', row.item.title, row.item.id, row.syncInfo.sync_disabled_reason);
} else {
section.body.push(_('Item "%s" could not be downloaded: %s', row.syncInfo.item_id, row.syncInfo.sync_disabled_reason));
msg = _('Item "%s" could not be downloaded: %s', row.syncInfo.item_id, row.syncInfo.sync_disabled_reason);
}
section.body.push({
text: msg,
canRetry: true,
canRetryType: CanRetryType.ItemSync,
retryHandler: async () => {
await BaseItem.saveSyncEnabled(row.item.type_, row.item.id);
},
});
}
section.body.push({ type: ReportItemType.CloseList });
sections.push(section);
}
@@ -150,7 +192,7 @@ export default class ReportService {
section.body.push({
text: _('%s: %s', toTitleCase(BaseModel.modelTypeToName(row.type_)), row.id),
canRetry: true,
canRetryType: 'e2ee',
canRetryType: CanRetryType.E2EE,
retryHandler: async () => {
await DecryptionWorker.instance().clearDisabledItem(row.type_, row.id);
void DecryptionWorker.instance().scheduleStart();
@@ -158,11 +200,12 @@ export default class ReportService {
});
}
const retryHandlers: any[] = [];
const retryHandlers: Function[] = [];
for (let i = 0; i < section.body.length; i++) {
if (section.body[i].canRetry) {
retryHandlers.push(section.body[i].retryHandler);
const item: RerportItemOrString = section.body[i];
if (typeof item !== 'string' && item.canRetry) {
retryHandlers.push(item.retryHandler);
}
}
@@ -210,7 +253,7 @@ export default class ReportService {
section.body.push({
text: _('%s (%s): %s', row.resource_title, row.resource_id, row.fetch_error),
canRetry: true,
canRetryType: 'resourceDownload',
canRetryType: CanRetryType.ResourceDownload,
retryHandler: async () => {
await Resource.resetErrorStatus(row.resource_id);
void ResourceFetcher.instance().autoAddResources();

View File

@@ -0,0 +1,39 @@
import WhenClause from './WhenClause';
describe('WhenClause', function() {
test('should work with simple condition', async function() {
const wc = new WhenClause('test1 && test2');
expect(wc.evaluate({
test1: true,
test2: true,
})).toBe(true);
expect(wc.evaluate({
test1: true,
test2: false,
})).toBe(false);
});
test('should work with parenthesis', async function() {
const wc = new WhenClause('(test1 && test2) || test3 && (test4 && !test5)');
expect(wc.evaluate({
test1: true,
test2: true,
test3: true,
test4: true,
test5: true,
})).toBe(true);
expect(wc.evaluate({
test1: false,
test2: true,
test3: false,
test4: false,
test5: true,
})).toBe(false);
});
});

View File

@@ -1,17 +1,71 @@
import { ContextKeyExpr, ContextKeyExpression } from './contextkey/contextkey';
import { ContextKeyExpr, ContextKeyExpression, IContext } from './contextkey/contextkey';
// We would like to support expressions with brackets but VSCode When Clauses
// don't support this. To support this, we split the expressions with brackets
// into sub-expressions, which can then be parsed and executed separately by the
// When Clause library.
interface AdvancedExpression {
// (test1 && test2) || test3
original: string;
// __sub_1 || test3
compiledText: string;
// { __sub_1: "test1 && test2" }
subExpressions: any;
}
function parseAdvancedExpression(advancedExpression: string): AdvancedExpression {
let subExpressionIndex = -1;
let subExpressions: string = '';
let currentSubExpressionKey = '';
const subContext: any = {};
let inBrackets = false;
for (let i = 0; i < advancedExpression.length; i++) {
const c = advancedExpression[i];
if (c === '(') {
if (inBrackets) throw new Error('Nested brackets not supported');
inBrackets = true;
subExpressionIndex++;
currentSubExpressionKey = `__sub_${subExpressionIndex}`;
subContext[currentSubExpressionKey] = '';
continue;
}
if (c === ')') {
if (!inBrackets) throw new Error('Closing bracket without an opening one');
inBrackets = false;
subExpressions += currentSubExpressionKey;
currentSubExpressionKey = '';
continue;
}
if (inBrackets) {
subContext[currentSubExpressionKey] += c;
} else {
subExpressions += c;
}
}
return {
compiledText: subExpressions,
subExpressions: subContext,
original: advancedExpression,
};
}
export default class WhenClause {
private expression_: string;
private expression_: AdvancedExpression;
private validate_: boolean;
private rules_: ContextKeyExpression = null;
private ruleCache_: Record<string, ContextKeyExpression> = {};
constructor(expression: string, validate: boolean) {
this.expression_ = expression;
public constructor(expression: string, validate: boolean = true) {
this.expression_ = parseAdvancedExpression(expression);
this.validate_ = validate;
}
private createContext(ctx: any) {
private createContext(ctx: any): IContext {
return {
getValue: (key: string) => {
return ctx[key];
@@ -19,21 +73,28 @@ export default class WhenClause {
};
}
private get rules(): ContextKeyExpression {
if (!this.rules_) {
this.rules_ = ContextKeyExpr.deserialize(this.expression_);
}
return this.rules_;
private rules(exp: string): ContextKeyExpression {
if (this.ruleCache_[exp]) return this.ruleCache_[exp];
this.ruleCache_[exp] = ContextKeyExpr.deserialize(exp);
return this.ruleCache_[exp];
}
public evaluate(context: any): boolean {
if (this.validate_) this.validate(context);
return this.rules.evaluate(this.createContext(context));
const subContext: any = {};
for (const k in this.expression_.subExpressions) {
const subExp = this.expression_.subExpressions[k];
subContext[k] = this.rules(subExp).evaluate(this.createContext(context));
}
const fullContext = { ...context, ...subContext };
return this.rules(this.expression_.compiledText).evaluate(this.createContext(fullContext));
}
public validate(context: any) {
const keys = this.rules.keys();
const keys = this.rules(this.expression_.original.replace(/[()]/g, ' ')).keys();
for (const key of keys) {
if (!(key in context)) throw new Error(`No such key: ${key}`);
}

View File

@@ -27,6 +27,7 @@ export interface WhenClauseContext {
noteIsHtml: boolean;
folderIsShareRootAndOwnedByUser: boolean;
folderIsShared: boolean;
joplinServerConnected: boolean;
}
export default function stateToWhenClauseContext(state: State, options: WhenClauseContextOptions = null): WhenClauseContext {
@@ -42,7 +43,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
// const commandNoteId = options.commandNoteId || selectedNoteId;
// const commandNote:NoteEntity = commandNoteId ? BaseModel.byId(state.notes, commandNoteId) : null;
const commandFolderId = options.commandFolderId;
const commandFolderId = options.commandFolderId || state.selectedFolderId;
const commandFolder: FolderEntity = commandFolderId ? BaseModel.byId(state.folders, commandFolderId) : null;
return {
@@ -75,5 +76,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
// Current context folder
folderIsShareRootAndOwnedByUser: commandFolder ? isRootSharedFolder(commandFolder) && isSharedFolderOwner(state, commandFolder.id) : false,
folderIsShared: commandFolder ? !!commandFolder.share_id : false,
joplinServerConnected: state.settings['sync.target'] === 9,
};
}

View File

@@ -27,11 +27,12 @@ export interface Command {
execute(...args: any[]): Promise<any | void>;
/**
* Defines whether the command should be enabled or disabled, which in turns affects
* the enabled state of any associated button or menu item.
* Defines whether the command should be enabled or disabled, which in turns
* affects the enabled state of any associated button or menu item.
*
* The condition should be expressed as a "when-clause" (as in Visual Studio Code). It's a simple boolean expression that evaluates to
* `true` or `false`. It supports the following operators:
* The condition should be expressed as a "when-clause" (as in Visual Studio
* Code). It's a simple boolean expression that evaluates to `true` or
* `false`. It supports the following operators:
*
* Operator | Symbol | Example
* -- | -- | --
@@ -40,7 +41,17 @@ export interface Command {
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
* And | && | "oneNoteSelected && !inConflictFolder"
*
* Currently the supported context variables aren't documented, but you can [find the list here](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts).
* Joplin, unlike VSCode, also supports parenthesis, which allows creating
* more complex expressions such as `cond1 || (cond2 && cond3)`. Only one
* level of parenthesis is possible (nested ones aren't supported).
*
* Currently the supported context variables aren't documented, but you can
* find the list below:
*
* - [Global When
* Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts).
* - [Desktop app When
* Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts).
*
* Note: Commands are enabled by default unless you use this property.
*/

View File

@@ -104,7 +104,7 @@ export default class ShareService {
await Folder.updateAllShareIds();
}
public async shareNote(noteId: string) {
public async shareNote(noteId: string): Promise<StateShare> {
const note = await Note.load(noteId);
if (!note) throw new Error(`No such note: ${noteId}`);
@@ -115,6 +115,24 @@ export default class ShareService {
return share;
}
public async unshareNote(noteId: string) {
const note = await Note.load(noteId);
if (!note) throw new Error(`No such note: ${noteId}`);
const shares = await this.refreshShares();
const noteShares = shares.filter(s => s.note_id === noteId);
const promises: Promise<void>[] = [];
for (const share of noteShares) {
promises.push(this.deleteShare(share.id));
}
await Promise.all(promises);
await Note.save({ id: note.id, is_shared: 0 });
}
public shareUrl(share: StateShare): string {
return `${this.api().baseUrl()}/shares/${share.id}`;
}
@@ -170,13 +188,15 @@ export default class ShareService {
});
}
public async refreshShares() {
public async refreshShares(): Promise<StateShare[]> {
const result = await this.loadShares();
this.store.dispatch({
type: 'SHARE_SET',
shares: result.items,
});
return result.items;
}
public async refreshShareUsers(shareId: string) {

View File

@@ -11,7 +11,7 @@ const theme: Theme = {
oddBackgroundColor: '#eeeeee',
color: '#32373F', // For regular text
colorError: 'red',
colorWarn: '#9A5B00',
colorWarn: 'rgb(228 86 0)',
colorFaded: '#7C8B9E', // For less important text
colorBright: '#000000', // For important text
dividerColor: '#dddddd',

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "1.8.2",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -91,3 +91,13 @@ From `packages/server`, run `npm run start-dev`
# Changelog
[View the changelog](https://github.com/laurent22/joplin/blob/dev/readme/changelog_server.md)
# License
Copyright (c) 2017-2021 Laurent Cozic
Personal Use License
Joplin Server is available for personal use only. For example you may host the software on your own server for non-commercial activity.
To obtain a license for commercial purposes, please contact us.

View File

@@ -6726,6 +6726,11 @@
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
},
"pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
},
"pretty-format": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz",

View File

@@ -32,6 +32,7 @@
"node-env-file": "^0.1.8",
"nodemon": "^2.0.6",
"pg": "^8.5.1",
"pretty-bytes": "^5.6.0",
"query-string": "^6.8.3",
"sqlite3": "^4.1.0",
"yargs": "^14.0.0"

View File

@@ -43,4 +43,8 @@ table.table .nowrap {
table.table .stretch {
width: 100%;
}
table.table th .sort-button i {
margin-left: 0.5rem;
}

Binary file not shown.

View File

@@ -237,9 +237,8 @@ export function changeTypeToString(t: ChangeType): string {
}
export enum ShareType {
Link = 1, // When a note is shared via a public link
App = 2, // When a note is shared with another user on the same server instance
JoplinRootFolder = 3,
Note = 1, // When a note is shared via a public link
Folder = 3, // When a complete folder is shared with another Joplin Server user
}
export enum ShareUserStatus {
@@ -276,6 +275,7 @@ export interface User extends WithDates, WithUuid {
password?: string;
full_name?: string;
is_admin?: number;
item_max_size?: number;
}
export interface Session extends WithDates, WithUuid {
@@ -378,6 +378,7 @@ export const databaseSchema: DatabaseTables = {
is_admin: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
item_max_size: { type: 'number' },
},
sessions: {
id: { type: 'string' },

View File

@@ -52,6 +52,7 @@ export default async function(ctx: AppContext) {
content: {
error,
stack: ctx.env === Env.Dev ? error.stack : '',
owner: ctx.owner,
},
};
ctx.response.body = await mustache().renderView(view);

View File

@@ -0,0 +1,12 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
export async function up(db: DbConnection): Promise<any> {
await db.schema.alterTable('users', function(table: Knex.CreateTableBuilder) {
table.integer('item_max_size').defaultTo(0).notNullable();
});
}
export async function down(_db: DbConnection): Promise<any> {
}

View File

@@ -30,7 +30,7 @@ describe('ChangeModel', function() {
const item1 = await createFolder(session.id, { title: 'folder' });
{
const changes = (await changeModel.allForUser(user.id)).items;
const changes = (await changeModel.delta(user.id)).items;
expect(changes.length).toBe(1);
expect(changes[0].item_id).toBe(item1.id);
expect(changes[0].type).toBe(ChangeType.Create);
@@ -60,7 +60,7 @@ describe('ChangeModel', function() {
// We don't get CREATE 1 because item 1 has been deleted. And we
// also don't get any UPDATE event since they've been compressed
// down to the CREATE events.
const changes = (await changeModel.allForUser(user.id)).items;
const changes = (await changeModel.delta(user.id)).items;
expect(changes.length).toBe(2);
expect(changes[0].item_id).toBe(item2.id);
expect(changes[0].type).toBe(ChangeType.Create);
@@ -89,7 +89,7 @@ describe('ChangeModel', function() {
//
// Then CREATE 1 is removed since item 1 has been deleted and UPDATE
// 2a is compressed down to CREATE 2.
const page1 = (await changeModel.allForUser(user.id, pagination));
const page1 = (await changeModel.delta(user.id, pagination));
let changes = page1.items;
expect(changes.length).toBe(1);
expect(page1.has_more).toBe(true);
@@ -98,7 +98,7 @@ describe('ChangeModel', function() {
// In the second page, we get all the expected events since nothing
// has been compressed.
const page2 = (await changeModel.allForUser(user.id, { ...pagination, cursor: page1.cursor }));
const page2 = (await changeModel.delta(user.id, { ...pagination, cursor: page1.cursor }));
changes = page2.items;
expect(changes.length).toBe(3);
// Although there are no more changes, it's not possible to know
@@ -112,7 +112,7 @@ describe('ChangeModel', function() {
expect(changes[2].type).toBe(ChangeType.Create);
// Check that we indeed reached the end of the feed.
const page3 = (await changeModel.allForUser(user.id, { ...pagination, cursor: page2.cursor }));
const page3 = (await changeModel.delta(user.id, { ...pagination, cursor: page2.cursor }));
expect(page3.items.length).toBe(0);
expect(page3.has_more).toBe(false);
}
@@ -127,7 +127,7 @@ describe('ChangeModel', function() {
await msleep(1); const item1 = await makeTestItem(user.id, 1); // CREATE 1
await msleep(1); await itemModel.saveForUser(user.id, { id: item1.id, name: `test_mod${i++}` }); // UPDATE 1
await expectThrow(async () => changeModel.allForUser(user.id, { limit: 1, cursor: 'invalid' }), 'resyncRequired');
await expectThrow(async () => changeModel.delta(user.id, { limit: 1, cursor: 'invalid' }), 'resyncRequired');
});
});

View File

@@ -1,8 +1,9 @@
import { Knex } from 'knex';
import { Change, ChangeType, Item, Uuid } from '../db';
import { md5 } from '../utils/crypto';
import { ErrorResyncRequired } from '../utils/errors';
import BaseModel, { SaveOptions } from './BaseModel';
import { PaginatedResults } from './utils/pagination';
import { PaginatedResults, Pagination, PaginationOrderDir } from './utils/pagination';
export interface ChangeWithItem {
item: Item;
@@ -26,17 +27,13 @@ export interface ChangePreviousItem {
jop_share_id: string;
}
export function defaultChangePagination(): ChangePagination {
export function defaultDeltaPagination(): ChangePagination {
return {
limit: 100,
cursor: '',
};
}
interface AllForUserOptions {
compressChanges?: boolean;
}
export default class ChangeModel extends BaseModel<Change> {
public get tableName(): string {
@@ -71,24 +68,7 @@ export default class ChangeModel extends BaseModel<Change> {
return results;
}
public async allForUser(userId: Uuid, pagination: ChangePagination = null, options: AllForUserOptions = null): Promise<PaginatedChanges> {
options = {
compressChanges: true,
...options,
};
pagination = {
...defaultChangePagination(),
...pagination,
};
let changeAtCursor: Change = null;
if (pagination.cursor) {
changeAtCursor = await this.load(pagination.cursor) as Change;
if (!changeAtCursor) throw new ErrorResyncRequired();
}
private changesForUserQuery(userId: Uuid): Knex.QueryBuilder {
// When need to get:
//
// - All the CREATE and DELETE changes associated with the user
@@ -98,7 +78,7 @@ export default class ChangeModel extends BaseModel<Change> {
// UPDATE changes do not have the user_id set because they are specific
// to the item, not to a particular user.
const query = this
return this
.db('changes')
.select([
'id',
@@ -116,8 +96,53 @@ export default class ChangeModel extends BaseModel<Change> {
// https://github.com/knex/knex/issues/1851
.orWhereRaw('type = ? AND item_id IN (SELECT item_id FROM user_items WHERE user_id = ?)', [ChangeType.Update, userId]);
});
}
// If a cursor was provided, apply it to both queries.
public async allByUser(userId: Uuid, pagination: Pagination = null): Promise<PaginatedChanges> {
pagination = {
page: 1,
limit: 100,
order: [{ by: 'counter', dir: PaginationOrderDir.ASC }],
...pagination,
};
const query = this.changesForUserQuery(userId);
const countQuery = query.clone();
const itemCount = (await countQuery.countDistinct('id', { as: 'total' }))[0].total;
void query
.orderBy(pagination.order[0].by, pagination.order[0].dir)
.offset((pagination.page - 1) * pagination.limit)
.limit(pagination.limit) as any[];
const changes = await query;
return {
items: changes,
// If we have changes, we return the ID of the latest changes from which delta sync can resume.
// If there's no change, we return the previous cursor.
cursor: changes.length ? changes[changes.length - 1].id : pagination.cursor,
has_more: changes.length >= pagination.limit,
page_count: itemCount !== null ? Math.ceil(itemCount / pagination.limit) : undefined,
};
}
public async delta(userId: Uuid, pagination: ChangePagination = null): Promise<PaginatedChanges> {
pagination = {
...defaultDeltaPagination(),
...pagination,
};
let changeAtCursor: Change = null;
if (pagination.cursor) {
changeAtCursor = await this.load(pagination.cursor) as Change;
if (!changeAtCursor) throw new ErrorResyncRequired();
}
const query = this.changesForUserQuery(userId);
// If a cursor was provided, apply it to the query.
if (changeAtCursor) {
void query.where('counter', '>', changeAtCursor.counter);
}
@@ -128,7 +153,7 @@ export default class ChangeModel extends BaseModel<Change> {
const changes = await query;
const finalChanges = options.compressChanges ? await this.removeDeletedItems(this.compressChanges(changes)) : changes;
const finalChanges = await this.removeDeletedItems(this.compressChanges(changes));
return {
items: finalChanges,

View File

@@ -3,7 +3,7 @@ import { ItemType, databaseSchema, Uuid, Item, ShareType, Share, ChangeType, Use
import { defaultPagination, paginateDbQuery, PaginatedResults, Pagination } from './utils/pagination';
import { isJoplinItemName, isJoplinResourceBlobPath, linkedResourceIds, serializeJoplinItem, unserializeJoplinItem } from '../utils/joplinUtils';
import { ModelType } from '@joplin/lib/BaseModel';
import { ApiError, ErrorForbidden, ErrorNotFound, ErrorUnprocessableEntity } from '../utils/errors';
import { ApiError, ErrorForbidden, ErrorNotFound, ErrorPayloadTooLarge, ErrorUnprocessableEntity } from '../utils/errors';
import { Knex } from 'knex';
import { ChangePreviousItem } from './ChangeModel';
@@ -282,10 +282,10 @@ export default class ItemModel extends BaseModel<Item> {
return this.itemToJoplinItem(raw);
}
public async saveFromRawContent(userId: Uuid, name: string, buffer: Buffer, options: ItemSaveOption = null): Promise<Item> {
public async saveFromRawContent(user: User, name: string, buffer: Buffer, options: ItemSaveOption = null): Promise<Item> {
options = options || {};
const existingItem = await this.loadByName(userId, name);
const existingItem = await this.loadByName(user.id, name);
const isJoplinItem = isJoplinItemName(name);
let isNote = false;
@@ -322,8 +322,14 @@ export default class ItemModel extends BaseModel<Item> {
if (options.shareId) item.jop_share_id = options.shareId;
// If the item is encrypted, we apply a multipler because encrypted
// items can be much larger (seems to be up to twice the size but for
// safety let's go with 2.2).
const maxSize = user.item_max_size * (item.jop_encryption_applied ? 2.2 : 1);
if (maxSize && buffer.byteLength > maxSize) throw new ErrorPayloadTooLarge();
return this.withTransaction<Item>(async () => {
const savedItem = await this.saveForUser(userId, item);
const savedItem = await this.saveForUser(user.id, item);
if (isNote) {
await this.models().itemResource().deleteByItemId(savedItem.id);
@@ -378,7 +384,8 @@ export default class ItemModel extends BaseModel<Item> {
public async childrenCount(userId: Uuid, pathQuery: string = ''): Promise<number> {
const query = this.childrenQuery(userId, pathQuery);
return query.count();
const r = await query.countDistinct('items.id', { as: 'total' });
return r[0].total;
}
private async joplinItemPath(jopId: string): Promise<Item[]> {
@@ -415,7 +422,7 @@ export default class ItemModel extends BaseModel<Item> {
const path = await this.joplinItemPath(jopId);
if (!path.length) throw new ApiError(`Cannot retrieve path for item: ${jopId}`, null, 'noPathForItem');
const rootFolderItem = path[path.length - 1];
const share = await this.models().share().itemShare(ShareType.JoplinRootFolder, rootFolderItem.id);
const share = await this.models().share().itemShare(ShareType.Folder, rootFolderItem.id);
if (!share) return null;
return {

View File

@@ -27,7 +27,7 @@ describe('ShareModel', function() {
error = await checkThrowAsync(async () => await models().share().createShare(user.id, 20 as ShareType, item.id));
expect(error instanceof ErrorBadRequest).toBe(true);
error = await checkThrowAsync(async () => await models().share().createShare(user.id, ShareType.Link, 'doesntexist'));
error = await checkThrowAsync(async () => await models().share().createShare(user.id, ShareType.Note, 'doesntexist'));
expect(error instanceof ErrorNotFound).toBe(true);
});
@@ -49,14 +49,14 @@ describe('ShareModel', function() {
});
const folderItem1 = await models().item().loadByJopId(user1.id, '000000000000000000000000000000F1');
await shareWithUserAndAccept(session1.id, session3.id, user3, ShareType.JoplinRootFolder, folderItem1);
await shareWithUserAndAccept(session1.id, session3.id, user3, ShareType.Folder, folderItem1);
const folderItem2 = await models().item().loadByJopId(user2.id, '000000000000000000000000000000F2');
await shareWithUserAndAccept(session2.id, session1.id, user1, ShareType.JoplinRootFolder, folderItem2);
await shareWithUserAndAccept(session2.id, session1.id, user1, ShareType.Folder, folderItem2);
const shares1 = await models().share().byUserId(user1.id, ShareType.JoplinRootFolder);
const shares2 = await models().share().byUserId(user2.id, ShareType.JoplinRootFolder);
const shares3 = await models().share().byUserId(user3.id, ShareType.JoplinRootFolder);
const shares1 = await models().share().byUserId(user1.id, ShareType.Folder);
const shares2 = await models().share().byUserId(user2.id, ShareType.Folder);
const shares3 = await models().share().byUserId(user3.id, ShareType.Folder);
expect(shares1.length).toBe(2);
expect(shares1.find(s => s.folder_id === '000000000000000000000000000000F1')).toBeTruthy();
@@ -69,4 +69,35 @@ describe('ShareModel', function() {
expect(shares3.find(s => s.folder_id === '000000000000000000000000000000F1')).toBeTruthy();
});
test('should generate only one link per shared note', async function() {
const { user: user1 } = await createUserAndSession(1);
await createItemTree(user1.id, '', {
'000000000000000000000000000000F1': {
'00000000000000000000000000000001': null,
},
});
const share1 = await models().share().shareNote(user1, '00000000000000000000000000000001');
const share2 = await models().share().shareNote(user1, '00000000000000000000000000000001');
expect(share1.id).toBe(share2.id);
});
test('should delete a note that has been shared', async function() {
const { user: user1 } = await createUserAndSession(1);
await createItemTree(user1.id, '', {
'000000000000000000000000000000F1': {
'00000000000000000000000000000001': null,
},
});
await models().share().shareNote(user1, '00000000000000000000000000000001');
const noteItem = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');
await models().item().delete(noteItem.id);
expect(await models().item().load(noteItem.id)).toBeFalsy();
});
});

View File

@@ -16,7 +16,7 @@ export default class ShareModel extends BaseModel<Share> {
if (action === AclAction.Create) {
if (!await this.models().item().userHasItem(user.id, resource.item_id)) throw new ErrorForbidden('cannot share an item not owned by the user');
if (resource.type === ShareType.JoplinRootFolder) {
if (resource.type === ShareType.Folder) {
const item = await this.models().item().loadByJopId(user.id, resource.folder_id);
if (item.jop_parent_id) throw new ErrorForbidden('A shared notebook must be at the root');
}
@@ -44,8 +44,8 @@ export default class ShareModel extends BaseModel<Share> {
}
protected async validate(share: Share, options: ValidateOptions = {}): Promise<Share> {
if ('type' in share && ![ShareType.Link, ShareType.App, ShareType.JoplinRootFolder].includes(share.type)) throw new ErrorBadRequest(`Invalid share type: ${share.type}`);
if (share.type !== ShareType.Link && await this.itemIsShared(share.type, share.item_id)) throw new ErrorBadRequest('A shared item cannot be shared again');
if ('type' in share && ![ShareType.Note, ShareType.Folder].includes(share.type)) throw new ErrorBadRequest(`Invalid share type: ${share.type}`);
if (share.type !== ShareType.Note && await this.itemIsShared(share.type, share.item_id)) throw new ErrorBadRequest('A shared item cannot be shared again');
const item = await this.models().item().load(share.item_id);
if (!item) throw new ErrorNotFound(`Could not find item: ${share.item_id}`);
@@ -241,295 +241,6 @@ export default class ShareModel extends BaseModel<Share> {
}
}
// public async updateSharedItems2(userId: Uuid) {
// const shares = await this.models().share().byUserId(userId, ShareType.JoplinRootFolder);
// if (!shares.length) return;
// const existingShareUserItems: UserItem[] = await this.models().userItem().itemsInShare(userId);
// const allShareUserItems: UserItem[] = [];
// for (const share of shares) {
// allShareUserItems.push({
// item_id: share.item_id,
// share_id: share.id,
// });
// const shareUserIds = await this.models().share().allShareUserIds(share);
// const shareItems = await this.models().item().sharedFolderChildrenItems(shareUserIds, share.folder_id);
// for (const item of shareItems) {
// allShareUserItems.push({
// item_id: item.id,
// share_id: share.id,
// });
// }
// }
// const userItemsToDelete: UserItem[] = [];
// for (const userItem of existingShareUserItems) {
// if (!allShareUserItems.find(ui => ui.item_id === userItem.item_id && ui.share_id === userItem.share_id)) {
// userItemsToDelete.push(userItem);
// }
// }
// const userItemsToCreate: UserItem[] = [];
// for (const userItem of allShareUserItems) {
// if (!existingShareUserItems.find(ui => ui.item_id === userItem.item_id && ui.share_id === userItem.share_id)) {
// userItemsToCreate.push(userItem);
// }
// }
// await this.withTransaction(async () => {
// await this.models().userItem().deleteByUserItemIds(userItemsToDelete.map(ui => ui.id));
// for (const userItem of userItemsToCreate) {
// await this.models().userItem().add(userId, userItem.item_id, userItem.share_id);
// }
// });
// }
// public async updateSharedItems() {
// enum ResourceChangeAction {
// Added = 1,
// Removed = 2,
// }
// interface ResourceChange {
// resourceIds: string[];
// share: Share;
// change: Change;
// action: ResourceChangeAction;
// }
// let resourceChanges: ResourceChange[] = [];
// const getSharedRootInfo = async (jopId: string) => {
// try {
// const output = await this.models().item().joplinItemSharedRootInfo(jopId);
// return output;
// } catch (error) {
// // "noPathForItem" means that the note or folder doesn't have a
// // parent yet. It can happen because items are synchronized in
// // random order, sometimes the children before the parents. In
// // that case we simply ignore the error, which means that for
// // now the share status of the item is unknown. The situation
// // will be resolved when the parent is received because in this
// // case, if the folder is within a shared folder, all its
// // children will be shared.
// if (error.code === 'noPathForItem') return null;
// throw error;
// }
// };
// const handleAddedToSharedFolder = async (item: Item, shareInfo: SharedRootInfo) => {
// const userIds = await this.allShareUserIds(shareInfo.share);
// for (const userId of userIds) {
// // If it's a folder we share its content. This is to ensure
// // that children that were synced before their parents get their
// // share status updated.
// // if (item.jop_type === ModelType.Folder) {
// // await this.models().item().shareJoplinFolderAndContent(shareInfo.share.id, shareInfo.share.owner_id, userId, item.jop_id);
// // }
// try {
// await this.models().userItem().add(userId, item.id);
// } catch (error) {
// if (isUniqueConstraintError(error)) {
// // Ignore - it means this user already has this item
// } else {
// throw error;
// }
// }
// }
// };
// const handleRemovedFromSharedFolder = async (change: Change, item: Item, shareInfo: SharedRootInfo) => {
// // This is called when a note parent ID changes and is moved out of
// // the shared folder. In that case, we need to unshare the item from
// // all users, except the one who did the action.
// //
// // - User 1 shares a folder with user 2
// // - User 2 moves a note out of the shared folder
// // - User 1 should no longer see the note. User 2 still sees it
// // since they have moved it to one of their own folders.
// const userIds = await this.allShareUserIds(shareInfo.share);
// for (const userId of userIds) {
// if (change.user_id !== userId) {
// await this.models().userItem().remove(userId, item.id);
// }
// }
// };
// const handleResourceSharing = async (change: Change, previousItem: ChangePreviousItem, item: Item, previousShareInfo: SharedRootInfo, currentShareInfo: SharedRootInfo) => {
// // Not a note - we can exit
// if (item.jop_type !== ModelType.Note) return;
// // Item was not in a shared folder and is still not in one - nothing to do
// if (!previousShareInfo && !currentShareInfo) return;
// // Item was moved out of a shared folder to a non-shared folder - unshare all resources
// if (previousShareInfo && !currentShareInfo) {
// resourceChanges.push({
// action: ResourceChangeAction.Removed,
// change,
// share: previousShareInfo.share,
// resourceIds: await this.models().itemResource().byItemId(item.id),
// });
// return;
// }
// // Item was moved from a non-shared folder to a shared one - share all resources
// if (!previousShareInfo && currentShareInfo) {
// resourceChanges.push({
// action: ResourceChangeAction.Added,
// change,
// share: currentShareInfo.share,
// resourceIds: await this.models().itemResource().byItemId(item.id),
// });
// return;
// }
// // Note either stayed in the same shared folder, or moved to another
// // shared folder. In that case, we check the note content before and
// // after and see if resources have been added or removed from it,
// // then we share/unshare resources based on this.
// const previousResourceIds = previousItem ? previousItem.jop_resource_ids : [];
// const currentResourceIds = await this.models().itemResource().byItemId(item.id);
// for (const resourceId of previousResourceIds) {
// if (!currentResourceIds.includes(resourceId)) {
// resourceChanges.push({
// action: ResourceChangeAction.Removed,
// change,
// share: currentShareInfo.share,
// resourceIds: [resourceId],
// });
// }
// }
// for (const resourceId of currentResourceIds) {
// if (!previousResourceIds.includes(resourceId)) {
// resourceChanges.push({
// action: ResourceChangeAction.Added,
// change,
// share: currentShareInfo.share,
// resourceIds: [resourceId],
// });
// }
// }
// };
// const handleCreatedItem = async (change: Change, item: Item) => {
// if (!item.jop_parent_id) return;
// const shareInfo = await getSharedRootInfo(item.jop_parent_id);
// if (!shareInfo) return;
// await handleResourceSharing(change, null, item, null, shareInfo);
// await handleAddedToSharedFolder(item, shareInfo);
// };
// const handleUpdatedItem = async (change: Change, item: Item) => {
// if (![ModelType.Note, ModelType.Folder].includes(item.jop_type)) return;
// const previousItem = this.models().change().unserializePreviousItem(change.previous_item);
// const previousShareInfo = previousItem?.jop_parent_id ? await getSharedRootInfo(previousItem.jop_parent_id) : null;
// const currentShareInfo = item.jop_parent_id ? await getSharedRootInfo(item.jop_parent_id) : null;
// await handleResourceSharing(change, previousItem, item, previousShareInfo, currentShareInfo);
// // Item was not in a shared folder and is still not in one
// if (!previousShareInfo && !currentShareInfo) return;
// // Item was in a shared folder and is still in the same shared folder
// if (previousShareInfo && currentShareInfo && previousShareInfo.item.jop_parent_id === currentShareInfo.item.jop_parent_id) return;
// // Item was not previously in a shared folder but has been moved to one
// if (!previousShareInfo && currentShareInfo) {
// await handleAddedToSharedFolder(item, currentShareInfo);
// return;
// }
// // Item was in a shared folder and is no longer in one
// if (previousShareInfo && !currentShareInfo) {
// await handleRemovedFromSharedFolder(change, item, previousShareInfo);
// return;
// }
// // Item was in a shared folder and has been moved to a different shared folder
// if (previousShareInfo && currentShareInfo && previousShareInfo.item.jop_parent_id !== currentShareInfo.item.jop_parent_id) {
// await handleRemovedFromSharedFolder(change, item, previousShareInfo);
// await handleAddedToSharedFolder(item, currentShareInfo);
// return;
// }
// // Sanity check - because normally all cases are covered above
// throw new Error('Unreachable');
// };
// // This loop essentially applies the change made by one user to all the
// // other users in the share.
// //
// // While it's processing changes, it's going to create new user_item
// // objects, which in turn generate more Change items, which are processed
// // again. However there are guards to ensure that it doesn't result in
// // an infinite loop - in particular once a user_item has been added,
// // adding it again will result in a UNIQUE constraint error and thus it
// // won't generate a Change object the second time.
// //
// // Rather than checking if the user_item exists before creating it, we
// // create it directly and let it fail, while catching the Unique error.
// // This is probably safer in terms of avoiding race conditions and
// // possibly faster.
// while (true) {
// const latestProcessedChange = await this.models().keyValue().value<string>('ShareService::latestProcessedChange');
// const changes = await this.models().change().allFromId(latestProcessedChange || '');
// if (!changes.length) break;
// const items = await this.models().item().loadByIds(changes.map(c => c.item_id));
// await this.withTransaction(async () => {
// for (const change of changes) {
// if (change.type === ChangeType.Create) {
// await handleCreatedItem(change, items.find(i => i.id === change.item_id));
// }
// if (change.type === ChangeType.Update) {
// await handleUpdatedItem(change, items.find(i => i.id === change.item_id));
// }
// // We don't need to handle ChangeType.Delete because when an
// // item is deleted, all its associated userItems are deleted
// // too.
// }
// for (const rc of resourceChanges) {
// const shareUserIds = await this.allShareUserIds(rc.share);
// const doShare = rc.action === ResourceChangeAction.Added;
// const changerUserId = rc.change.user_id;
// for (const shareUserId of shareUserIds) {
// // We apply the updates to all the users, except the one
// // who made the change, since they already have the
// // change.
// if (shareUserId === changerUserId) continue;
// await this.updateResourceShareStatus(doShare, rc.share.id, changerUserId, shareUserId, rc.resourceIds);
// }
// }
// resourceChanges = [];
// await this.models().keyValue().setValue('ShareService::latestProcessedChange', changes[changes.length - 1].id);
// });
// }
// }
public async updateResourceShareStatus(doShare: boolean, _shareId: Uuid, changerUserId: Uuid, toUserId: Uuid, resourceIds: string[]) {
const resourceItems = await this.models().item().loadByJopIds(changerUserId, resourceIds);
const resourceBlobNames = resourceIds.map(id => resourceBlobPath(id));
@@ -588,39 +299,25 @@ export default class ShareModel extends BaseModel<Share> {
if (share) return share;
const shareToSave = {
type: ShareType.JoplinRootFolder,
type: ShareType.Folder,
item_id: folderItem.id,
owner_id: owner.id,
folder_id: folderId,
};
await this.checkIfAllowed(owner, AclAction.Create, shareToSave);
// const shareItems = await this.models().item().sharedFolderChildrenItems([owner.id], folderId);
// const userItems = await this.models().userItem().byItemIds(shareItems.map(s => s.id));
// const userItemIds = userItems.map(u => u.id);
return super.save(shareToSave);
// return this.withTransaction(async () => {
// const savedShare = await super.save(shareToSave);
// await this
// .db('user_items')
// .whereIn('id', userItemIds)
// .orWhere('item_id', '=', shareToSave.item_id)
// .update({ share_id: savedShare.id });
// return savedShare;
// });
}
public async shareNote(owner: User, noteId: string): Promise<Share> {
const noteItem = await this.models().item().loadByJopId(owner.id, noteId);
if (!noteItem) throw new ErrorNotFound(`No such note: ${noteId}`);
const existingShare = await this.byItemId(noteItem.id);
if (existingShare) return existingShare;
const shareToSave = {
type: ShareType.Link,
type: ShareType.Note,
item_id: noteItem.id,
owner_id: owner.id,
note_id: noteId,

View File

@@ -1,4 +1,4 @@
import { Item, Share, ShareUser, ShareUserStatus, User, Uuid } from '../db';
import { Item, Share, ShareType, ShareUser, ShareUserStatus, User, Uuid } from '../db';
import { ErrorForbidden, ErrorNotFound } from '../utils/errors';
import BaseModel, { AclAction, DeleteOptions } from './BaseModel';
@@ -123,24 +123,15 @@ export default class ShareUserModel extends BaseModel<ShareUser> {
return this.save({ ...shareUser, status });
});
// const item = await this.models().item().load(share.item_id);
// return this.withTransaction<Item>(async () => {
// await this.save({ ...shareUser, status });
// if (status === ShareUserStatus.Accepted) {
// if (share.type === ShareType.JoplinRootFolder) {
// // await this.models().item().shareJoplinFolderAndContent(share.id, share.owner_id, userId, item.jop_id);
// } else if (share.type === ShareType.App) {
// await this.models().userItem().add(userId, share.item_id, share.id);
// }
// }
// });
}
public async deleteByShare(share: Share): Promise<void> {
// Notes that are shared by link do not have associated ShareUser items,
// so there's nothing to do.
if (share.type !== ShareType.Folder) return;
const shareUsers = await this.byShareId(share.id, null);
if (!shareUsers.length) return;
await this.withTransaction(async () => {
await this.delete(shareUsers.map(s => s.id));

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