You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
30 Commits
server-v2.
...
android-v2
Author | SHA1 | Date | |
---|---|---|---|
|
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
|
||||
|
@@ -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',
|
||||
|
9
.gitignore
vendored
9
.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
|
||||
|
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
|
||||
|
@@ -8,9 +8,8 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"audit": "lerna-audit",
|
||||
"bootstrap": "lerna bootstrap --no-ci",
|
||||
"bootstrapIgnoreScripts": "lerna bootstrap --ignore-scripts --no-ci",
|
||||
"bootstrapServerOnly": "lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/server",
|
||||
"bootstrap": "lerna bootstrap --force-local --no-ci",
|
||||
"bootstrapServerOnly": "lerna bootstrap --force-local --no-ci --include-dependents --include-dependencies --scope @joplin/server",
|
||||
"build": "lerna run build && npm run tsc",
|
||||
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildDoc": "./packages/tools/build-all.sh",
|
||||
|
@@ -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",
|
||||
@@ -31,7 +31,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.8.1",
|
||||
"version": "2.0.0",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
|
@@ -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
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "1.8.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
@@ -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": "1.8.5",
|
||||
"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 "1.8.5"
|
||||
versionCode 2097632
|
||||
versionName "2.0.1"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -492,7 +492,7 @@
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 10.8.1;
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -519,7 +519,7 @@
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 10.8.1;
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -666,7 +666,7 @@
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 10.8.1;
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -697,7 +697,7 @@
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 10.8.1;
|
||||
MARKETING_VERSION = 20.0.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"id": "<%= pluginId %>",
|
||||
"app_min_version": "1.8",
|
||||
"app_min_version": "2.0",
|
||||
"version": "1.0.0",
|
||||
"name": "<%= pluginName %>",
|
||||
"description": "<%= pluginDescription %>",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "1.8.1",
|
||||
"version": "2.0.0",
|
||||
"description": "Scaffolds out a new Joplin plugin",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||
"author": {
|
||||
@@ -34,4 +34,4 @@
|
||||
"repository": "https://github.com/laurent22/generator-joplin",
|
||||
"license": "MIT",
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
|
@@ -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');
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/lib",
|
||||
"version": "1.8.2",
|
||||
"version": "2.0.0",
|
||||
"description": "Joplin Core library",
|
||||
"author": "Laurent Cozic",
|
||||
"homepage": "",
|
||||
|
@@ -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',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/plugin-repo-cli",
|
||||
"version": "1.8.2",
|
||||
"version": "2.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/renderer",
|
||||
"version": "1.8.2",
|
||||
"version": "2.0.0",
|
||||
"description": "The Joplin note renderer, used the mobile and desktop application",
|
||||
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
|
||||
"main": "index.js",
|
||||
|
@@ -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.
|
||||
|
2
packages/server/package-lock.json
generated
2
packages/server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "1.8.0",
|
||||
"version": "2.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/server",
|
||||
"version": "1.8.0",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start-dev": "nodemon --config nodemon.json dist/app.js --env dev",
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
|
@@ -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.count('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,
|
||||
|
@@ -378,6 +378,7 @@ export default class ItemModel extends BaseModel<Item> {
|
||||
|
||||
public async childrenCount(userId: Uuid, pathQuery: string = ''): Promise<number> {
|
||||
const query = this.childrenQuery(userId, pathQuery);
|
||||
query.groupBy('items.id');
|
||||
return query.count();
|
||||
}
|
||||
|
||||
@@ -415,7 +416,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 {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user