You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-26 23:38:08 +02:00
Compare commits
36 Commits
release-se
...
server_ite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1652d57d7 | ||
|
|
ec7f0f479a | ||
|
|
7f05420fda | ||
|
|
a3f8cd4850 | ||
|
|
01ccf5170a | ||
|
|
6ddb69e1ea | ||
|
|
b01f82bb33 | ||
|
|
b6c9edba21 | ||
|
|
f7d164be6e | ||
|
|
6f2f24171d | ||
|
|
12cc64008b | ||
|
|
b9955f58d3 | ||
|
|
489995daef | ||
|
|
e156ee1b58 | ||
|
|
a24b0091ad | ||
|
|
2655b6deee | ||
|
|
45c40f7395 | ||
|
|
ecb0eee355 | ||
|
|
4916f4cc92 | ||
|
|
15fe119256 | ||
|
|
0b46880a00 | ||
|
|
deaa731983 | ||
|
|
d061bb1a4f | ||
|
|
03db0c5486 | ||
|
|
bb275e671d | ||
|
|
2d0580ff71 | ||
|
|
2331d3487b | ||
|
|
f1380fd51d | ||
|
|
d462dab8eb | ||
|
|
cf37b74d9a | ||
|
|
aec3ea9c0c | ||
|
|
1f5aa70acd | ||
|
|
416637ce83 | ||
|
|
99c4b0bc01 | ||
|
|
74d8fec98a | ||
|
|
321a58c356 |
@@ -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
|
||||
|
||||
@@ -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
12
.gitignore
vendored
@@ -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
|
||||
|
||||
76
README.md
76
README.md
@@ -511,47 +511,47 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
 | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 98%
|
||||
 | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 31%
|
||||
 | 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%
|
||||
 | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 59%
|
||||
 | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 84%
|
||||
 | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 99%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 98%
|
||||
 | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 58%
|
||||
 | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 96%
|
||||
 | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 30%
|
||||
 | 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%
|
||||
 | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 58%
|
||||
 | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | jmontane, 2019 | 83%
|
||||
 | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 96%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Atalanttore](mailto:atalanttore@googlemail.com) | 95%
|
||||
 | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 57%
|
||||
 | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
 | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 33%
|
||||
 | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 97%
|
||||
 | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 94%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 97%
|
||||
 | 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%
|
||||
 | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 94%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 73%
|
||||
 | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 97%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 68%
|
||||
 | 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%
|
||||
 | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 63%
|
||||
 | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 46%
|
||||
 | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
|
||||
 | 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%
|
||||
 | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 97%
|
||||
 | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 84%
|
||||
 | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 97%
|
||||
 | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 73%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [Yang Zhang](mailto:zyangmath@gmail.com) | 97%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 95%
|
||||
 | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 98%
|
||||
 | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 99%
|
||||
 | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato | 94%
|
||||
 | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 99%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Alessandro Bernardello](mailto:mailfilledwithspam@gmail.com) | 94%
|
||||
 | 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%
|
||||
 | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 92%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 71%
|
||||
 | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [konhi](mailto:hello.konhi@gmail.com) | 94%
|
||||
 | 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%
|
||||
 | 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%
|
||||
 | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 66%
|
||||
 | 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%
|
||||
 | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 61%
|
||||
 | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 45%
|
||||
 | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 73%
|
||||
 | 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%
|
||||
 | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 94%
|
||||
 | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 97%
|
||||
 | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Sergey Segeda](mailto:thesermanarm@gmail.com) | 94%
|
||||
 | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 71%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [Yang Zhang](mailto:zyangmath@gmail.com) | 94%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Yaoze Ye](mailto:yaozeye@yahoo.co.jp) | 92%
|
||||
 | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
|
||||
 | 한국어 | [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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
@@ -1,7 +1,9 @@
|
||||
Subshell:
|
||||
|
||||
(
|
||||
set -e
|
||||
false
|
||||
echo Unreachable
|
||||
) && echo Great success
|
||||
```
|
||||
(
|
||||
set -e
|
||||
false
|
||||
echo Unreachable
|
||||
) && echo Great success
|
||||
```
|
||||
@@ -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`
|
||||
1
packages/app-cli/tests/enex_to_md/code4.html
Normal file
1
packages/app-cli/tests/enex_to_md/code4.html
Normal 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> System.out.println('Hello World');</span><br/></div><div>}</div></div><div><br/></div><div>end of code block</div></div>
|
||||
9
packages/app-cli/tests/enex_to_md/code4.md
Normal file
9
packages/app-cli/tests/enex_to_md/code4.md
Normal file
@@ -0,0 +1,9 @@
|
||||
code block:
|
||||
|
||||
```
|
||||
public static void main(String[] args) {
|
||||
System.out.println('Hello World');
|
||||
}
|
||||
```
|
||||
|
||||
end of code block
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
enabledCondition: 'folderIsShareRootAndOwnedByUser || !folderIsShared',
|
||||
enabledCondition: 'joplinServerConnected && (folderIsShareRootAndOwnedByUser || !folderIsShared)',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,5 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
enabledCondition: 'joplinServerConnected && someNotesSelected',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -45,5 +45,7 @@ export default function() {
|
||||
'editor.swapLineUp',
|
||||
'editor.swapLineDown',
|
||||
'toggleSafeMode',
|
||||
'showShareNoteDialog',
|
||||
'showShareFolderDialog',
|
||||
];
|
||||
}
|
||||
|
||||
9
packages/app-desktop/gui/style/StyledLink.tsx
Normal file
9
packages/app-desktop/gui/style/StyledLink.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
2
packages/app-desktop/package-lock.json
generated
2
packages/app-desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "1.8.5",
|
||||
"version": "2.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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, ')');
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
2
packages/lib/package-lock.json
generated
2
packages/lib/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "1.8.2",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -12,6 +12,7 @@ const imageMimeTypes = [
|
||||
'image/png',
|
||||
'image/prs.btif',
|
||||
'image/prs.pti',
|
||||
'image/svg+xml',
|
||||
'image/t38',
|
||||
'image/tiff',
|
||||
'image/tiff-fx',
|
||||
|
||||
@@ -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();
|
||||
|
||||
39
packages/lib/services/WhenClause.test.ts
Normal file
39
packages/lib/services/WhenClause.test.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
2
packages/renderer/package-lock.json
generated
2
packages/renderer/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "1.8.2",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
5
packages/server/package-lock.json
generated
5
packages/server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
@@ -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' },
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user