Compare commits
236 Commits
v1.0.175
...
cli-v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
|
1abff212f9 | ||
|
df63572b7c | ||
|
d89071dc03 | ||
|
95e0e8d459 | ||
|
6973952892 | ||
|
56cf5271a2 | ||
|
d679ceeb9b | ||
|
49cb391486 | ||
|
cfdde4c2ce | ||
|
57864a388a | ||
|
74c8a38d48 | ||
|
cd19cedd46 | ||
|
3f23d8ed06 | ||
|
8cbb0d03e8 | ||
|
52b99a1520 | ||
|
7eabe74402 | ||
|
10cf80d6ca | ||
|
bccfd0bcbd | ||
|
fa9e2bd6dd | ||
|
b15b3d6ac5 | ||
|
c4fb5b72cd | ||
|
fd706c3dbc | ||
|
5128190942 | ||
|
c6f127b48e | ||
|
011d66356f | ||
|
d24a974219 | ||
|
82f5e26ef4 | ||
|
247182edbf | ||
|
69fc518e39 | ||
|
aef4a88d7f | ||
|
69e70d88f4 | ||
|
aac0a45beb | ||
|
d04d89d622 | ||
|
0f1633dfbf | ||
|
737c3f62db | ||
|
49701fbc55 | ||
|
5395d57df8 | ||
|
8a7e3fe36f | ||
|
7bc0a52cc9 | ||
|
f428cc26a2 | ||
|
cf6c141e57 | ||
|
3a29b5f321 | ||
|
ffdae41605 | ||
|
688edd4b32 | ||
|
d687ef5c09 | ||
|
59c8a87047 | ||
|
759d59c2e6 | ||
|
73d12e1ed5 | ||
|
fcda843778 | ||
|
dcbd8aed30 | ||
|
154c838e9f | ||
|
f90f688299 | ||
|
fca9b57af5 | ||
|
59eed8395d | ||
|
a4ccd2d43a | ||
|
5136e7a0e0 | ||
|
423243c84b | ||
|
2042deb2bf | ||
|
e1216dce4b | ||
|
3839c7818e | ||
|
90652e40b4 | ||
|
83c1c20ce3 | ||
|
0bb1484b2d | ||
|
5881cee167 | ||
|
101935e594 | ||
|
9dda65de20 | ||
|
a00e35fb57 | ||
|
2c85b55ff8 | ||
|
d1b51b409a | ||
|
c3d5463589 | ||
|
710447f879 | ||
|
c61e4cae4d | ||
|
333aebf32c | ||
|
2657c8736e | ||
|
5b28f6b25f | ||
|
715253da2f | ||
|
66356d83ab | ||
|
8e531ca87a | ||
|
18c46851fd | ||
|
5456dbbf16 | ||
|
5c54b83108 | ||
|
cbf7f03bff | ||
|
ea05fea234 | ||
|
f78729ad1f | ||
|
4ec9492f7c | ||
|
f86b953420 | ||
|
d8f91a2ece | ||
|
6563606799 | ||
|
c01bc1c363 | ||
|
6f8c634756 | ||
|
22a93994aa | ||
|
e0013858c4 | ||
|
b6e0df57eb | ||
|
be210233be | ||
|
1a1a1d3841 | ||
|
4283bbde7f | ||
|
fba325f60e | ||
|
fcd76dabac | ||
|
f661cad6a3 | ||
|
1faac68441 | ||
|
e9366a0d41 | ||
|
953aa5d0b5 | ||
|
fc5782990f | ||
|
01163783ef | ||
|
be19a92f59 | ||
|
3fed1abc36 | ||
|
6973bf9331 | ||
|
e8867fa0f1 | ||
|
d9c15b84d0 | ||
|
81876c7bf3 | ||
|
ce6c7c8783 | ||
|
fad2ff674e | ||
|
1dd7727e97 | ||
|
fe0318584e | ||
|
8508fe737b | ||
|
c7a9e5f656 | ||
|
3e43fbce13 | ||
|
b304e2ae1f | ||
|
35f4ede11a | ||
|
65cbb6e388 | ||
|
960d7f84eb | ||
|
8a392e1c06 | ||
|
d9d75d6c71 | ||
|
69f9e38730 | ||
|
7f95186a97 | ||
|
b6db2bf2c5 | ||
|
6f976abf42 | ||
|
d80ffeeba1 | ||
|
c856e8d9ac | ||
|
6736bda429 | ||
|
0a8f9163db | ||
|
e078de25f0 | ||
|
cd284f78ad | ||
|
0a13c988fa | ||
|
b61bfd6ffe | ||
|
fc61b474cd | ||
|
bf25364333 | ||
|
bc7099d29b | ||
|
00c3ed715c | ||
|
701b57de89 | ||
|
e674d7d23b | ||
|
4a2d9bb028 | ||
|
ae3a278ac4 | ||
|
42ada7123c | ||
|
6d9f73eef7 | ||
|
541372eb91 | ||
|
8d7d70bc13 | ||
|
e77cc18468 | ||
|
193978a8be | ||
|
853ac0cca8 | ||
|
589f0803e6 | ||
|
fc67a44f95 | ||
|
204365b2ae | ||
|
2a63ecef2a | ||
|
1d660d7141 | ||
|
69000c0fc5 | ||
|
c8a0138b3b | ||
|
90de63e650 | ||
|
6b6e17cbad | ||
|
071bd2b0ca | ||
|
f74db06176 | ||
|
a6b3ddc7ed | ||
|
4ff889d4ec | ||
|
12b9f1b969 | ||
|
59bb1015ab | ||
|
f9c77171cf | ||
|
9628b64d3e | ||
|
d3f47a38b8 | ||
|
8111213691 | ||
|
a88ff902b4 | ||
|
1e57e1e486 | ||
|
172afb0789 | ||
|
5bfd1849c1 | ||
|
f61c9c93bb | ||
|
b0efdb6ee8 | ||
|
888a9ddaf4 | ||
|
d2482d6554 | ||
|
21cac248b3 | ||
|
ce7671151c | ||
|
b77525e570 | ||
|
e93cc50d1c | ||
|
c534305c7b | ||
|
797b71d903 | ||
|
74fd9e1e9e | ||
|
ff94a95589 | ||
|
0f5192bf19 | ||
|
eabbbba0c7 | ||
|
840cdf5512 | ||
|
757a6854ab | ||
|
b16dd051f1 | ||
|
68e73b658a | ||
|
af5f301276 | ||
|
2b9818a94d | ||
|
58200ecdb1 | ||
|
acaf22fa11 | ||
|
f60d6e0748 | ||
|
ae9163e9bb | ||
|
cad6b7971f | ||
|
ee38590c35 | ||
|
f10695fb8f | ||
|
b44ecc1958 | ||
|
931e7a7795 | ||
|
df85bb189d | ||
|
27e1f53b5f | ||
|
266ddedaef | ||
|
44dd327d22 | ||
|
13be56a2e3 | ||
|
5d0ba460ae | ||
|
6988b3accb | ||
|
3e2676a8c6 | ||
|
0f88c947f1 | ||
|
07b175c2ee | ||
|
6132cf2128 | ||
|
37dbb81425 | ||
|
c1028ec2cf | ||
|
f98dc4e576 | ||
|
da044960f9 | ||
|
03522b48a5 | ||
|
1b31525773 | ||
|
64a1408d6c | ||
|
3a5e68fca0 | ||
|
48ce788118 | ||
|
34f0a2951a | ||
|
66546418e3 | ||
|
611be7c0fa | ||
|
4f3e031f4f | ||
|
554c46182a | ||
|
b5d5d02a9c | ||
|
4640b65b85 | ||
|
1615c6bdc8 | ||
|
c003b8d32d | ||
|
3a1f924fb1 | ||
|
583460c0a8 | ||
|
1550a52002 | ||
|
9bd3bc8404 | ||
|
8c1d13b364 |
@@ -45,4 +45,11 @@ Server/docs/
|
||||
Server/dist/
|
||||
Server/bin/
|
||||
Server/node_modules/
|
||||
ElectronClient/app/packageInfo.js
|
||||
ElectronClient/app/packageInfo.js
|
||||
ReactNativeClient/pluginAssets/
|
||||
ReactNativeClient/lib/joplin-renderer/vendor/fountain.min.js
|
||||
|
||||
# Ignore files generated from TypeScript files
|
||||
ElectronClient/app/gui/ShareNoteDialog.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
14
.eslintrc.js
@@ -6,6 +6,11 @@ module.exports = {
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
'extends': ['eslint:recommended'],
|
||||
"settings": {
|
||||
'react': {
|
||||
'version': '16.12',
|
||||
},
|
||||
},
|
||||
'globals': {
|
||||
'Atomics': 'readonly',
|
||||
'SharedArrayBuffer': 'readonly',
|
||||
@@ -44,7 +49,11 @@ module.exports = {
|
||||
// This error is always a false positive so far since it detects
|
||||
// possible race conditions in contexts where we know it cannot happen.
|
||||
"require-atomic-updates": 0,
|
||||
// "no-lonely-if": "error",
|
||||
|
||||
// -------------------------------
|
||||
// Coding style preferences
|
||||
// -------------------------------
|
||||
"enforce-react-hooks/enforce-react-hooks": 2,
|
||||
|
||||
// -------------------------------
|
||||
// Formatting
|
||||
@@ -59,6 +68,8 @@ module.exports = {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"prefer-template": ["error"],
|
||||
"template-curly-spacing": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"key-spacing": ["error", {
|
||||
"beforeColon": false,
|
||||
"afterColon": true,
|
||||
@@ -81,5 +92,6 @@ module.exports = {
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint",
|
||||
"enforce-react-hooks",
|
||||
],
|
||||
};
|
6
.gitignore
vendored
@@ -44,3 +44,9 @@ ElectronClient/app/gui/note-viewer/fonts/
|
||||
ElectronClient/app/gui/note-viewer/lib.js
|
||||
Tools/commit_hook.txt
|
||||
.vscode/*
|
||||
*.map
|
||||
|
||||
# Ignore files generated from TypeScript files
|
||||
ElectronClient/app/gui/ShareNoteDialog.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
19
.travis.yml
@@ -50,12 +50,17 @@ before_install:
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y yarn
|
||||
sudo apt-get install -y gettext
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
# Copy lib
|
||||
rsync -aP --delete ReactNativeClient/lib/ ElectronClient/app/lib/
|
||||
|
||||
# Install tools
|
||||
npm install
|
||||
npm run tsc
|
||||
cd Tools
|
||||
npm install
|
||||
cd ..
|
||||
@@ -84,6 +89,19 @@ script:
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate translations - this is needed as some users manually
|
||||
# edit .po files (and often make mistakes) instead of using a proper
|
||||
# tool like poedit. Doing it for Linux only is sufficient.
|
||||
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
if [ "$TRAVIS_OS_NAME" != "osx" ]; then
|
||||
node Tools/validate-translation.js
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Find out if we should run the build or not. Electron-builder gets stuck when
|
||||
# builing PRs so we disable it in this case. The Linux build should provide
|
||||
# enough info if the app builds or not.
|
||||
@@ -96,5 +114,4 @@ script:
|
||||
|
||||
# Prepare the Electron app and build it
|
||||
cd ElectronClient/app
|
||||
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||
npm install && USE_HARD_LINKS=false yarn dist
|
||||
|
BIN
Assets/GitHubSponsorIcon.png
Normal file
After Width: | Height: | Size: 19 KiB |
31
Assets/JoplinIconBlack.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg40"
|
||||
version="1.1"
|
||||
width="1536"
|
||||
height="1536"
|
||||
viewBox="0 0 1536 1536">
|
||||
<metadata
|
||||
id="metadata46">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs44" />
|
||||
<path
|
||||
id="path38"
|
||||
fill="#ffffff"
|
||||
d="M 373.834,0 C 168.227,0 0,168.223 0,373.834 V 1162.17 C 0,1367.778 168.227,1536 373.834,1536 H 1162.17 C 1367.778,1536 1536,1367.778 1536,1162.17 V 373.834 C 1536,168.224 1367.778,0 1162.17,0 Z m 397.222,205.431 h 417.424 a 7.132,7.132 0 0 1 7.132,7.133 v 132.552 c 0,4.461 -3.619,8.073 -8.077,8.073 h -57.23 c -24.168,0 -43.768,19.338 -44.284,43.374 v 2.377 h -0.017 v 136.191 h -0.053 l -0.466,509.375 c -5.02,77.667 -39.222,149.056 -96.324,201.046 -60.28,54.834 -141.948,85.017 -229.962,85.017 -12.45,0 -25.208,-0.61 -37.907,-1.785 -92.157,-8.682 -181.494,-48.601 -251.662,-112.438 -71.99,-65.517 -117.147,-150.03 -127.164,-238 -11.226,-98.763 23.42,-192.783 95.045,-257.937 81.99,-74.637 198.185,-101.768 316.613,-75.704 5.574,1.227 9.55,6.282 9.55,11.997 v 199.52 c -0.199,2.625 -1.481,6.599 -8.183,2.896 -0.663,-0.365 -1.194,-0.511 -1.653,-0.531 -21.987,-10.587 -45.159,-17.57 -68.559,-19.916 -0.38,-0.04 -0.757,-0.124 -1.138,-0.163 -0.537,-0.048 -1.034,-0.033 -1.556,-0.075 -4.13,-0.354 -8.183,-0.517 -12.203,-0.58 -0.87,-0.011 -1.771,-0.127 -2.641,-0.127 -0.486,0 -0.951,0.05 -1.437,0.057 -1.464,0.011 -2.886,0.115 -4.33,0.163 -2.76,0.102 -5.497,0.211 -8.182,0.448 -0.273,0.024 -0.547,0.07 -0.835,0.097 -25.509,2.4 -47.864,11.104 -65.012,25.47 -0.954,0.802 -1.974,1.53 -2.9,2.36 a 1.34,1.34 0 0 1 -0.168,0.146 c -23.96,21.8 -34.881,53.872 -30.726,90.316 4.62,40.737 26.94,81.156 62.841,113.823 35.908,32.67 80.335,52.977 125.113,57.186 35.118,3.36 66.547,-3.919 89.899,-20.461 a 97.255,97.255 0 0 0 9.365,-7.501 c 2.925,-2.661 5.569,-5.5 8.086,-8.416 0.3,-0.348 0.672,-0.673 0.975,-1.024 8.253,-9.864 14.222,-21.067 17.996,-33.148 0.639,-2.034 1.051,-4.148 1.564,-6.227 0.381,-1.563 0.81,-3.106 1.112,-4.693 0.555,-2.784 0.923,-5.632 1.253,-8.49 0.086,-0.709 0.183,-1.414 0.237,-2.128 0.492,-4.893 0.693,-9.858 0.55,-14.91 h 0.013 V 393.623 c -2.01,-22.626 -20.78,-40.434 -43.928,-40.434 h -57.23 a 8.071,8.071 0 0 1 -8.077,-8.073 V 212.564 a 7.132,7.132 0 0 1 7.136,-7.133 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
56
BUILD.md
@@ -5,6 +5,14 @@
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
|
||||
- In general, most of the backend (anything to do with the database, synchronisation, data import or export, etc.) is shared across all the apps, so when making a change please consider how it will affect all the apps.
|
||||
|
||||
# TypeScript
|
||||
|
||||
Most of the application is written in JavaScript, however new classes and files should generally be written in [TypeScript](https://www.typescriptlang.org/). Even if you don't write TypeScript code, you will need to build the existing .ts and .tsx files. This is done from the root of the project, by running `npm run tsc`.
|
||||
|
||||
If you are modifying TypeScript code, the best is to have the compiler watch for changes from a terminal. To do so, run `npm run tsc-watch`.
|
||||
|
||||
All TypeScript files are generated next to the .ts or .tsx file. So for example, if there's a file "lib/MyClass.ts", there will be a generated "lib/MyClass.js" next to it. If you create a new TypeScript file, make sure you add the generated .js file to .gitignore. It is implemented that way as it requires minimal changes to integrate TypeScript in the existing JavaScript code base.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
brew install yarn node
|
||||
@@ -14,7 +22,7 @@
|
||||
## Linux and Windows (WSL) dependencies
|
||||
|
||||
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- Install node v10.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the tools
|
||||
@@ -22,14 +30,17 @@
|
||||
Before building any of the applications, you need to build the tools and pre-commit hooks:
|
||||
|
||||
```
|
||||
npm install && cd Tools && npm install
|
||||
npm install && cd Tools && npm install && cd ..
|
||||
```
|
||||
|
||||
# Building the Electron application
|
||||
|
||||
## Linux and macOS
|
||||
|
||||
```
|
||||
npm run copyLib
|
||||
npm run tsc
|
||||
cd ElectronClient/app
|
||||
rsync --delete -a ../../ReactNativeClient/lib/ lib/
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
@@ -44,13 +55,14 @@ That will create the executable file in the `dist` directory.
|
||||
|
||||
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
|
||||
## Building Electron application on Windows
|
||||
## Windows
|
||||
|
||||
Run the following commands on Windows Command prompt running as Administrator:
|
||||
|
||||
```
|
||||
cd Tools
|
||||
npm install
|
||||
cd ..\ElectronClient\app
|
||||
xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
|
||||
xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
|
||||
npm run tsc
|
||||
cd ElectronClient\app
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
@@ -63,11 +75,36 @@ If you get an `error MSB8020: The build tools for v140 cannot be found.` try to
|
||||
|
||||
The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might be helpful.
|
||||
|
||||
## Troubleshooting desktop application
|
||||
|
||||
> The application window doesn't open or is white
|
||||
|
||||
This is an indication that there's an early initialisation error. Try this:
|
||||
|
||||
- In ElectronAppWrapper, set `debugEarlyBugs` to `true`. This will force the window to show up and should open the console next to it, which should display any error.
|
||||
- In more rare cases, an already open instance of Joplin can create strange low-level bugs that will display no error but will result in this white window. A non-dev instance of Joplin, or a dev instance that wasn't properly closed might cause this. So make sure you close everything and try again. Perhaps even other Electron apps running (Skype, Slack, etc.) could cause this?
|
||||
- Also try to delete node_modules and rebuild.
|
||||
- If all else fail, switch your computer off and on again, to make sure you start clean.
|
||||
|
||||
> How to work on the app from Windows?
|
||||
|
||||
You should not use WSL at all because this is a GUI app that lives outside of WSL, and the WSL layer can cause all kind of very hard to debug issues. It can also lock files in node_modules that cannot be unlocked when the app crashes (you need to restart your computer). Likewise, don't run the TypeScript watch command from WSL.
|
||||
|
||||
So everything should be done from a Windows Command prompt running as Administrator. You can use `run.bat` to run the app in dev mode.
|
||||
|
||||
# Building the Mobile application
|
||||
|
||||
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "React Native CLI Quickstart" tab.
|
||||
|
||||
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
Then:
|
||||
|
||||
```
|
||||
npm run tsc
|
||||
cd ReactNativeClient
|
||||
npm install
|
||||
react-native run-ios
|
||||
# Or: react-native run-android
|
||||
```
|
||||
|
||||
# Building the Terminal application
|
||||
|
||||
@@ -75,7 +112,6 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
|
||||
cd CliClient
|
||||
npm install
|
||||
./build.sh
|
||||
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
|
||||
```
|
||||
|
||||
Run `run.sh` to start the application for testing.
|
||||
|
@@ -7,18 +7,19 @@ The [Joplin Forum](https://discourse.joplinapp.org/) is the community driven pla
|
||||
File bugs in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Please follow these guidelines:
|
||||
|
||||
- Search existing issues first, make sure yours hasn't already been reported.
|
||||
- Please follow the template.
|
||||
- Consider [enabling debug mode](https://joplinapp.org/debugging/) so that you can provide as much details as possible when reporting the issue.
|
||||
- Stay on topic, but describe the issue in detail so that others can reproduce it.
|
||||
- Stay on topic, but describe the issue in detail so that others can **reproduce** it.
|
||||
- **Provide a screenshot** if possible. A screenshot showing the problem is often more useful than a paragraph describing it.
|
||||
- For web clipper bugs, **please provide the URL causing the issue**. Sometimes the clipper works in one page but not in another so it is important to know what URL has a problem.
|
||||
|
||||
# Feature requests
|
||||
|
||||
Please check that your request has not already been posted in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). If it has, **up-voting the issue** increases the chances it'll be noticed and implemented in the future. "+1" comments are not tracked.
|
||||
Feature requests **must be opened and discussed on the [forum](https://discourse.joplinapp.org/c/features)**. After they have been accepted, they can be added to the GitHub tracker.
|
||||
|
||||
As a general rule, suggestions to *improve Joplin* should be posted first in the [Joplin Forum](https://discourse.joplinapp.org/) for discussion.
|
||||
Please check that your request has not already been posted on the forum or the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). If it has, **up-voting the issue or topic** increases the chances it'll be noticed and implemented in the future. "+1" comments are not tracked.
|
||||
|
||||
Avoid listing multiple requests in one report in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). One issue per request makes it easier to track and discuss it.
|
||||
Avoid listing multiple requests in one topic. One topic per request makes it easier to track and discuss it.
|
||||
|
||||
Finally, when submitting a pull request, don't forget to [test your code](#unit-tests).
|
||||
|
||||
@@ -39,11 +40,13 @@ If you want to start contributing to the project's code, please follow these gui
|
||||
|
||||
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
|
||||
|
||||
## Coding style
|
||||
### Coding style
|
||||
|
||||
Coding style is enforced by a pre-commit hook that runs eslint. This hook is installed whenever running `npm install` on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running `npm install` at the root of the repository.
|
||||
|
||||
## Unit tests
|
||||
For new React components, please use [React Hooks](https://reactjs.org/docs/hooks-intro.html). For new code in general, please use TypeScript (unless you are modifying a file that was originally in JavaScript).
|
||||
|
||||
### Unit tests
|
||||
|
||||
When submitting a pull request for a new feature or bug fix, please add unit tests for your code. Unit testing GUI changes is not always possible so it is not required, but any change in a file under /lib for example should be unit tested.
|
||||
|
||||
@@ -51,24 +54,26 @@ The tests are under CliClient/tests. To get them running, you first need to buil
|
||||
|
||||
cd CliClient
|
||||
npm i
|
||||
./build.sh
|
||||
|
||||
To run the test units, you must have an instance of the cli app running. In a first window navigate into `CliClient` and run:
|
||||
|
||||
```sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
> If you get an error like `Error: Cannot find module '../locales/index.js'`, this means you must (a) rebuild translations or (b) take > them from one of the other apps. To do option b, you can run the following command to copy them from the `ReactNativeClient` directory:>
|
||||
>
|
||||
> ```sh
|
||||
> cd .. # Return to the root of the project
|
||||
> rsync -aP ./ReactNativeClient/locales/ ./CliClient/build/locales/
|
||||
> ```
|
||||
|
||||
Then run the tests in a second window. To run all the test units:
|
||||
To run all the test units:
|
||||
|
||||
./run_test.sh
|
||||
|
||||
To run just one particular file:
|
||||
|
||||
./run_test.sh markdownUtils # Don't add the .js extension
|
||||
|
||||
To filter tests:
|
||||
|
||||
./run_test.sh "should handle conflict" # Will run all the test units that contain "should handle conflict" in their description
|
||||
|
||||
## About abandoned pull requests
|
||||
|
||||
It happens that a pull request is started but not finished and despite our attempts to contact the contributor, we don’t hear from them again.
|
||||
|
||||
In that case we will not merge the pull request, even if only small changes are missing. Our policy is simply to close the pull request. Why? Because an unfinished pull request essentially means giving us work and moving on. We would rather not encourage this behaviour.
|
||||
|
||||
Also, please note that since we have spent time reviewing the pull request and proposing solutions, we reserve the right to re-use that knowledge to create a new pull request, potentially based on your changes.
|
||||
|
||||
We’d much prefer that you complete the pull request though, so we’ll be sure to ping you a few times before that!
|
||||
|
@@ -131,6 +131,24 @@ class Command extends BaseCommand {
|
||||
lines.push('');
|
||||
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/#searching');
|
||||
lines.push('');
|
||||
lines.push('To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.');
|
||||
lines.push('');
|
||||
lines.push('For example, to retrieve the notebook named `recipes`: **GET /search?query=recipes&type=folder**');
|
||||
lines.push('');
|
||||
lines.push('To retrieve all the tags that start with `project-`: **GET /search?query=project-*&type=tag**');
|
||||
lines.push('');
|
||||
|
||||
lines.push('# Item type IDs');
|
||||
lines.push('');
|
||||
lines.push('Item type IDs might be refered to in certain object you will retrieve from the API. This is the correspondance between name and ID:');
|
||||
lines.push('');
|
||||
lines.push('Name | Value');
|
||||
lines.push('---- | -----');
|
||||
for (const t of BaseModel.typeEnum_) {
|
||||
const value = t[1];
|
||||
lines.push(`${BaseModel.modelTypeToName(value)} | ${value} `);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
|
@@ -1,6 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { app } = require('./app.js');
|
||||
const fs = require('fs-extra');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -13,11 +14,60 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
options() {
|
||||
return [['-v, --verbose', _('Also displays unset and hidden config variables.')]];
|
||||
return [
|
||||
['-v, --verbose', _('Also displays unset and hidden config variables.')],
|
||||
['--export', 'Writes all settings to STDOUT as JSON including secure variables.'],
|
||||
['--import', 'Reads in JSON formatted settings from STDIN.'],
|
||||
['--import-file <file>', 'Reads in settings from <file>. <file> must contain valid JSON.'],
|
||||
];
|
||||
}
|
||||
async __importSettings(inputStream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// being defensive and not attempting to settle twice
|
||||
let isSettled = false;
|
||||
const chunks = [];
|
||||
|
||||
inputStream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = inputStream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
inputStream.on('end', () => {
|
||||
let json = chunks.join('');
|
||||
let settingsObj;
|
||||
try {
|
||||
settingsObj = JSON.parse(json);
|
||||
} catch (err) {
|
||||
isSettled = true;
|
||||
return reject(new Error(`Invalid JSON passed to config --import: \n${err.message}.`));
|
||||
}
|
||||
if (settingsObj) {
|
||||
Object.entries(settingsObj)
|
||||
.forEach(([key, value]) => {
|
||||
Setting.setValue(key, value);
|
||||
});
|
||||
}
|
||||
if (!isSettled) {
|
||||
isSettled = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
inputStream.on('error', (error) => {
|
||||
if (!isSettled) {
|
||||
isSettled = true;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
async action(args) {
|
||||
const verbose = args.options.verbose;
|
||||
const isExport = args.options.export;
|
||||
const isImport = args.options.import || args.options.importFile;
|
||||
const importFile = args.options.importFile;
|
||||
|
||||
const renderKeyValue = name => {
|
||||
const md = Setting.settingMetadata(name);
|
||||
@@ -32,35 +82,45 @@ class Command extends BaseCommand {
|
||||
}
|
||||
};
|
||||
|
||||
if (!args.name && !args.value) {
|
||||
if (isExport || (!isImport && !args.value)) {
|
||||
let keys = Setting.keys(!verbose, 'cli');
|
||||
keys.sort();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const value = Setting.value(keys[i]);
|
||||
if (!verbose && !value) continue;
|
||||
this.stdout(renderKeyValue(keys[i]));
|
||||
|
||||
if (isExport) {
|
||||
const resultObj = keys.reduce((acc, key) => {
|
||||
const value = Setting.value(key);
|
||||
if (!verbose && !value) return acc;
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
// Printing the object in "pretty" format so it's easy to read/edit
|
||||
this.stdout(JSON.stringify(resultObj, null, 2));
|
||||
} else if (!args.name) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const value = Setting.value(keys[i]);
|
||||
if (!verbose && !value) continue;
|
||||
this.stdout(renderKeyValue(keys[i]));
|
||||
}
|
||||
} else {
|
||||
this.stdout(renderKeyValue(args.name));
|
||||
}
|
||||
app()
|
||||
.gui()
|
||||
.showConsole();
|
||||
app()
|
||||
.gui()
|
||||
.maximizeConsole();
|
||||
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.name && !args.value) {
|
||||
this.stdout(renderKeyValue(args.name));
|
||||
app()
|
||||
.gui()
|
||||
.showConsole();
|
||||
app()
|
||||
.gui()
|
||||
.maximizeConsole();
|
||||
return;
|
||||
if (isImport) {
|
||||
let fileStream = process.stdin;
|
||||
if (importFile) {
|
||||
fileStream = fs.createReadStream(importFile, { autoClose: true });
|
||||
}
|
||||
await this.__importSettings(fileStream);
|
||||
} else {
|
||||
Setting.setValue(args.name, args.value);
|
||||
}
|
||||
|
||||
Setting.setValue(args.name, args.value);
|
||||
|
||||
if (args.name == 'locale') {
|
||||
setLocale(Setting.value('locale'));
|
||||
|
@@ -25,7 +25,7 @@ class Command extends BaseCommand {
|
||||
info: stdoutFn,
|
||||
warn: stdoutFn,
|
||||
error: stdoutFn,
|
||||
}});
|
||||
} });
|
||||
ClipperServer.instance().setDispatch(() => {});
|
||||
ClipperServer.instance().setLogger(clipperLogger);
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
|
||||
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
|
@@ -8,8 +8,8 @@ require('app-module-path').addPath(__dirname);
|
||||
|
||||
const compareVersion = require('compare-version');
|
||||
const nodeVersion = process && process.versions && process.versions.node ? process.versions.node : '0.0.0';
|
||||
if (compareVersion(nodeVersion, '8.0.0') < 0) {
|
||||
console.error(`Joplin requires Node 8+. Detected version ${nodeVersion}`);
|
||||
if (compareVersion(nodeVersion, '10.0.0') < 0) {
|
||||
console.error(`Joplin requires Node 10+. Detected version ${nodeVersion}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
@@ -7,4 +7,10 @@ rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
|
||||
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/locales/" "$BUILD_DIR/locales/"
|
||||
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
|
||||
|
||||
# Don't add TypeScript here or make it silent as output of Cli app must be clean
|
||||
# cd $ROOT_DIR/..
|
||||
# npm run tsc
|
||||
# cd $ROOT_DIR
|
||||
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
3146
CliClient/locales/pt_PT.po
Normal file
355
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "1.0.149",
|
||||
"version": "1.0.155",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -653,6 +653,11 @@
|
||||
"supports-color": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
|
||||
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
@@ -806,6 +811,11 @@
|
||||
"debug": "^2.6.9"
|
||||
}
|
||||
},
|
||||
"font-awesome-filetypes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/font-awesome-filetypes/-/font-awesome-filetypes-2.1.0.tgz",
|
||||
"integrity": "sha512-U6hi14GRjfZFIWsTNyVmCBuHyPhiizWEKVbaQqHipKQv3rA1l1PNvmKulzpqxonFnQMToty5ZhfWbc/0IjLDGA=="
|
||||
},
|
||||
"for-each-property": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/for-each-property/-/for-each-property-0.0.4.tgz",
|
||||
@@ -1027,9 +1037,9 @@
|
||||
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
|
||||
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
|
||||
"version": "9.18.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz",
|
||||
"integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg=="
|
||||
},
|
||||
"html-encoding-sniffer": {
|
||||
"version": "1.0.2",
|
||||
@@ -1131,9 +1141,9 @@
|
||||
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
|
||||
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz",
|
||||
"integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
@@ -1544,9 +1554,9 @@
|
||||
}
|
||||
},
|
||||
"joplin-turndown-plugin-gfm": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.11.tgz",
|
||||
"integrity": "sha512-S2I+VCTqIhpWKKkPHsyJ5rdll9H/JjMXoBVClRX1TnphcmrSxufevdoXWWVgLncdXpSSiuoifCXgFZy3ueVElg=="
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.12.tgz",
|
||||
"integrity": "sha512-qL4+1iycQjZ1fs8zk3jSRk7cg3ROBUHk7GKtiLAQLFzLPKErnILUvz5DLszSQvz3s1sTjPbywLDISVUtBY6HaA=="
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.1.2",
|
||||
@@ -1636,6 +1646,21 @@
|
||||
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
|
||||
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.11.1.tgz",
|
||||
"integrity": "sha512-5oANDICCTX0NqYIyAiFCCwjQ7ERu3DQG2JFHLbYOf+fXaMoH8eg/zOq5WSYJsKMi/QebW+Eh3gSM+oss1H/bww==",
|
||||
"requires": {
|
||||
"commander": "^2.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"klaw": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
|
||||
@@ -1669,9 +1694,9 @@
|
||||
}
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
|
||||
"integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
@@ -1700,6 +1725,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz",
|
||||
"integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4="
|
||||
},
|
||||
"lodash.repeat": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.repeat/-/lodash.repeat-4.1.0.tgz",
|
||||
"integrity": "sha1-/H3oEx2MisB+S0n3T/6CnR8r7EQ="
|
||||
},
|
||||
"lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
@@ -1730,6 +1760,13 @@
|
||||
"requires": {
|
||||
"fault": "^1.0.2",
|
||||
"highlight.js": "~9.12.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
|
||||
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
|
||||
}
|
||||
}
|
||||
},
|
||||
"magicli": {
|
||||
@@ -1744,17 +1781,104 @@
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
|
||||
"integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~1.1.1",
|
||||
"entities": "~2.0.0",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
|
||||
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown-it-abbr": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz",
|
||||
"integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g="
|
||||
},
|
||||
"markdown-it-anchor": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz",
|
||||
"integrity": "sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ=="
|
||||
},
|
||||
"markdown-it-deflist": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-deflist/-/markdown-it-deflist-2.0.3.tgz",
|
||||
"integrity": "sha512-/BNZ8ksW42bflm1qQLnRI09oqU2847Z7MVavrR0MORyKLtiUYOMpwtlAfMSZAQU9UCvaUZMpgVAqoS3vpToJxw=="
|
||||
},
|
||||
"markdown-it-emoji": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz",
|
||||
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
|
||||
},
|
||||
"markdown-it-expand-tabs": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-expand-tabs/-/markdown-it-expand-tabs-1.0.13.tgz",
|
||||
"integrity": "sha512-ODpk98FWkGIq2vkwm2NOLt4G6TRgy3M9eTa9SFm06pUyOd0zjjYAwkhsjiCDU42pzKuz0ChiwBO0utuOj3LNOA==",
|
||||
"requires": {
|
||||
"lodash.repeat": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"markdown-it-footnote": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz",
|
||||
"integrity": "sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A=="
|
||||
},
|
||||
"markdown-it-ins": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.0.tgz",
|
||||
"integrity": "sha512-+vyAdBuMGwmT2yMlAFJSx2VR/0QZ1onQ/Mkkmr4l9tDFOh5sVoAgRbkgbuSsk+sxJ9vaMH/IQ323ydfvQrPO/Q=="
|
||||
},
|
||||
"markdown-it-mark": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.0.tgz",
|
||||
"integrity": "sha512-HqMWeKfMMOu4zBO0emmxsoMWmbf2cPKZY1wP6FsTbKmicFfp5y4L3KXAsNeO1rM6NTJVOrNlLKMPjWzriBGspw=="
|
||||
},
|
||||
"markdown-it-multimd-table": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.0.1.tgz",
|
||||
"integrity": "sha512-ZgRV8LlGz6JXTZ5zd82yCL8IVG5MRastMWxxrc6hQC8aC8kq/7zpp+ksBqVqcdTmTdabnkuSo/7h3SyKM31YCA==",
|
||||
"requires": {
|
||||
"markdown-it": "^8.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"markdown-it": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
|
||||
"integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~1.1.1",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdown-it-sub": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz",
|
||||
"integrity": "sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g="
|
||||
},
|
||||
"markdown-it-sup": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz",
|
||||
"integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M="
|
||||
},
|
||||
"markdown-it-toc-done-right": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.1.0.tgz",
|
||||
"integrity": "sha512-UhD2Oj6cZV3ycYPoelt4hTkwKIK3zbPP1wjjdpCq7UGtWQOFalDFDv1s2zBYV6aR2gMs/X8kpJcOYsQmUbiXDw=="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
@@ -1859,9 +1983,9 @@
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw=="
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
|
||||
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.1",
|
||||
@@ -1887,27 +2011,27 @@
|
||||
}
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.3.1.tgz",
|
||||
"integrity": "sha512-CaLXV3W8Vnbps8ZANqDGz7j4x7Yj1LW4TWF/TQuDfj7Cfx4nAPTvw98qgTevtto1oHDrh3pQkaODbqupXlsWTg==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
|
||||
"integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
|
||||
"requires": {
|
||||
"debug": "^4.1.0",
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1996,14 +2120,22 @@
|
||||
}
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
|
||||
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g=="
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz",
|
||||
"integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==",
|
||||
"requires": {
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz",
|
||||
"integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA=="
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz",
|
||||
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz",
|
||||
"integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==",
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1"
|
||||
@@ -2226,7 +2358,7 @@
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
}
|
||||
}
|
||||
@@ -2676,137 +2808,13 @@
|
||||
"integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw="
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.7.tgz",
|
||||
"integrity": "sha512-TGEeSBB8O48bEu8KUUMqzeB22WrfTxzhIf0lFm8wLTo3a6yJBonF2sPKMYrYtOne1F1t9AHAEn+DTISq8WebQg==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz",
|
||||
"integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==",
|
||||
"requires": {
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "^0.11.0",
|
||||
"request": "^2.87.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
||||
"integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
|
||||
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"requires": {
|
||||
"ajv": "^6.5.5",
|
||||
"har-schema": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.0",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"requires": {
|
||||
"psl": "^1.1.24",
|
||||
"punycode": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"sshpk": {
|
||||
@@ -3162,9 +3170,9 @@
|
||||
"integrity": "sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0="
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz",
|
||||
"integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg=="
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.3.25",
|
||||
@@ -3205,6 +3213,11 @@
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
},
|
||||
"unorm": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
|
||||
"integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
|
||||
},
|
||||
"unpack-string": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unpack-string/-/unpack-string-0.0.2.tgz",
|
||||
@@ -3244,6 +3257,14 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"uslug": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uslug/-/uslug-1.0.4.tgz",
|
||||
"integrity": "sha1-uaIvCRTgqGFAYz2swwLl9PpFBnc=",
|
||||
"requires": {
|
||||
"unorm": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@@ -16,16 +16,17 @@
|
||||
2016,
|
||||
2017,
|
||||
2018,
|
||||
2019
|
||||
2019,
|
||||
2020
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.155",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.7.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
@@ -45,10 +46,10 @@
|
||||
"image-data-uri": "^2.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"joplin-turndown": "^4.0.19",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.11",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.12",
|
||||
"jssha": "^2.3.0",
|
||||
"levenshtein": "^1.0.5",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it": "^10.0.0",
|
||||
"md5": "^2.2.1",
|
||||
"mime": "^2.0.3",
|
||||
"moment": "^2.24.0",
|
||||
@@ -66,7 +67,7 @@
|
||||
"server-destroy": "^1.0.1",
|
||||
"sharp": "^0.23.2",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"sqlite3": "^4.0.7",
|
||||
"sqlite3": "^4.1.1",
|
||||
"string-padding": "^1.0.2",
|
||||
"string-to-stream": "^1.1.0",
|
||||
"strip-ansi": "^4.0.0",
|
||||
@@ -80,7 +81,24 @@
|
||||
"valid-url": "^1.0.9",
|
||||
"word-wrap": "^1.2.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"yargs-parser": "^7.0.0"
|
||||
"yargs-parser": "^7.0.0",
|
||||
"font-awesome-filetypes": "^2.1.0",
|
||||
"highlight.js": "^9.17.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"katex": "^0.11.1",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
"markdown-it-anchor": "^5.2.5",
|
||||
"markdown-it-deflist": "^2.0.3",
|
||||
"markdown-it-emoji": "^1.4.0",
|
||||
"markdown-it-expand-tabs": "^1.0.13",
|
||||
"markdown-it-footnote": "^3.0.2",
|
||||
"markdown-it-ins": "^3.0.0",
|
||||
"markdown-it-mark": "^3.0.0",
|
||||
"markdown-it-multimd-table": "^4.0.1",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-toc-done-right": "^4.1.0",
|
||||
"uslug": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jasmine": "^3.5.0"
|
||||
|
@@ -1,4 +1,7 @@
|
||||
#!/bin/bash
|
||||
echo "Deprecated! Use `node Tools/release-cli.js`"
|
||||
exit 1
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
41
CliClient/tests/MdToMd.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const mdImporterService = require('lib/services/InteropService_Importer_Md');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js');
|
||||
|
||||
const importer = new mdImporterService();
|
||||
|
||||
|
||||
describe('InteropService_Importer_Md: importLocalImages', function() {
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
it('should import linked files and modify tags appropriately', async function() {
|
||||
const tagNonExistentFile = '';
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(2);
|
||||
const inexistentLinkUnchanged = note.body.includes(tagNonExistentFile);
|
||||
expect(inexistentLinkUnchanged).toBe(true);
|
||||
});
|
||||
it('should only create 1 resource for duplicate links, all tags should be updated', async function() {
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample-duplicate-links.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(1);
|
||||
const reg = new RegExp(items[0].id, 'g');
|
||||
const matched = note.body.match(reg);
|
||||
expect(matched.length).toBe(2);
|
||||
});
|
||||
it('should import linked files and modify tags appropriately when link is also in alt text', async function() {
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample-link-in-alt-text.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(1);
|
||||
});
|
||||
it('should passthrough unchanged if no links present', async function() {
|
||||
const note = await importer.importFile(`${__dirname}/md_to_md/sample-no-links.md`, 'notebook');
|
||||
let items = await Note.linkedItems(note.body);
|
||||
expect(items.length).toBe(0);
|
||||
expect(note.body).toContain('Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.');
|
||||
});
|
||||
});
|
@@ -41,7 +41,7 @@ describe('Encryption', function() {
|
||||
};
|
||||
|
||||
const encodedHeader = service.encodeHeader_(header);
|
||||
const decodedHeader = service.decodeHeader_(encodedHeader);
|
||||
const decodedHeader = service.decodeHeaderBytes_(encodedHeader);
|
||||
delete decodedHeader.length;
|
||||
|
||||
expect(objectsEqual(header, decodedHeader)).toBe(true);
|
||||
@@ -54,14 +54,14 @@ describe('Encryption', function() {
|
||||
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await service.decryptMasterKey(masterKey, 'wrongpassword');
|
||||
await service.decryptMasterKey_(masterKey, 'wrongpassword');
|
||||
} catch (error) {
|
||||
hasThrown = true;
|
||||
}
|
||||
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456');
|
||||
const decryptedMasterKey = await service.decryptMasterKey_(masterKey, '123456');
|
||||
expect(decryptedMasterKey.length).toBe(512);
|
||||
}));
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('Encryption', function() {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
const cipherText = await service.encryptString('some secret');
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
@@ -87,11 +87,37 @@ describe('Encryption', function() {
|
||||
expect(plainText2 === veryLongSecret).toBe(true);
|
||||
}));
|
||||
|
||||
it('should decrypt various encryption methods', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
{
|
||||
const cipherText = await service.encryptString('some secret', {
|
||||
encryptionMethod: EncryptionService.METHOD_SJCL_2,
|
||||
});
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
expect(plainText).toBe('some secret');
|
||||
const header = await service.decodeHeaderString(cipherText);
|
||||
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_2);
|
||||
}
|
||||
|
||||
{
|
||||
const cipherText = await service.encryptString('some secret', {
|
||||
encryptionMethod: EncryptionService.METHOD_SJCL_3,
|
||||
});
|
||||
const plainText = await service.decryptString(cipherText);
|
||||
expect(plainText).toBe('some secret');
|
||||
const header = await service.decodeHeaderString(cipherText);
|
||||
expect(header.encryptionMethod).toBe(EncryptionService.METHOD_SJCL_3);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should fail to decrypt if master key not present', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
const cipherText = await service.encryptString('some secret');
|
||||
|
||||
@@ -107,7 +133,7 @@ describe('Encryption', function() {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
let cipherText = await service.encryptString('some secret');
|
||||
cipherText += 'ABCDEFGHIJ';
|
||||
@@ -120,7 +146,7 @@ describe('Encryption', function() {
|
||||
it('should encrypt and decrypt notes and folders', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
let folder = await Folder.save({ title: 'folder' });
|
||||
let note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id });
|
||||
@@ -151,7 +177,7 @@ describe('Encryption', function() {
|
||||
it('should encrypt and decrypt files', asyncTest(async () => {
|
||||
let masterKey = await service.generateMasterKey('123456');
|
||||
masterKey = await MasterKey.save(masterKey);
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
const sourcePath = `${__dirname}/../tests/support/photo.jpg`;
|
||||
const encryptedPath = `${__dirname}/data/photo.crypted`;
|
||||
@@ -164,4 +190,34 @@ describe('Encryption', function() {
|
||||
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
|
||||
}));
|
||||
|
||||
// it('should upgrade master key encryption mode', asyncTest(async () => {
|
||||
// let masterKey = await service.generateMasterKey('123456', {
|
||||
// encryptionMethod: EncryptionService.METHOD_SJCL_2,
|
||||
// });
|
||||
// masterKey = await MasterKey.save(masterKey);
|
||||
// Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||
// Setting.setValue('encryption.activeMasterKeyId', masterKey.id);
|
||||
|
||||
// await sleep(0.01);
|
||||
|
||||
// await service.loadMasterKeysFromSettings();
|
||||
|
||||
// masterKeyNew = await MasterKey.load(masterKey.id);
|
||||
|
||||
// // Check that the master key has been upgraded
|
||||
|
||||
// expect(masterKeyNew.created_time).toBe(masterKey.created_time);
|
||||
// expect(masterKeyNew.updated_time === masterKey.updated_time).toBe(false);
|
||||
// expect(masterKeyNew.content === masterKey.content).toBe(false);
|
||||
// expect(masterKeyNew.encryption_method === masterKey.encryption_method).toBe(false);
|
||||
// expect(masterKeyNew.checksum === masterKey.checksum).toBe(false);
|
||||
// expect(masterKeyNew.encryption_method).toBe(service.defaultMasterKeyEncryptionMethod_);
|
||||
|
||||
// // Check that encryption still works
|
||||
|
||||
// const cipherText = await service.encryptString('some secret');
|
||||
// const plainText = await service.decryptString(cipherText);
|
||||
// expect(plainText).toBe('some secret');
|
||||
// }));
|
||||
|
||||
});
|
||||
|
@@ -49,4 +49,19 @@ describe('markdownUtils', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
it('escape a markdown link (title)', asyncTest(async () => {
|
||||
|
||||
const testCases = [
|
||||
['Helmut K. C. Tessarek', 'Helmut K. C. Tessarek'],
|
||||
['Helmut (K. C.) Tessarek', 'Helmut (K. C.) Tessarek'],
|
||||
['Helmut [K. C.] Tessarek', 'Helmut \\[K. C.\\] Tessarek'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const md = testCases[i][0];
|
||||
const expected = testCases[i][1];
|
||||
expect(markdownUtils.escapeTitleText(md)).toBe(expected);
|
||||
}
|
||||
}));
|
||||
|
||||
});
|
||||
|
2
CliClient/tests/md_to_md/sample-duplicate-links.md
Normal file
@@ -0,0 +1,2 @@
|
||||

|
||||

|
3
CliClient/tests/md_to_md/sample-link-in-alt-text.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Markdown
|
||||
 should put resource link inside () not []
|
||||
 this case (spaces before/after link but within parens) is not currently covered
|
3
CliClient/tests/md_to_md/sample-no-links.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Markdown
|
||||
|
||||
Unidentified vessel travelling at sub warp speed, bearing 235.7. Fluctuations in energy readings from it, Captain. All transporters off.
|
13
CliClient/tests/md_to_md/sample.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Markdown
|
||||
|
||||
lorem ipsum 
|
||||
- [ ] check!
|
||||
- [ ] boxes!
|
||||
|
||||
ipsum lorem
|
||||
|
||||
**strong text**
|
||||
 lorem ipsum
|
||||
|
||||
**some directory**
|
||||
 lorem ipsum
|
@@ -150,4 +150,31 @@ describe('models_Folder', function() {
|
||||
expect(foldersById[f4.id].note_count).toBe(0);
|
||||
}));
|
||||
|
||||
it('should not count completed to-dos', asyncTest(async () => {
|
||||
|
||||
let f1 = await Folder.save({ title: 'folder1' });
|
||||
let f2 = await Folder.save({ title: 'folder2', parent_id: f1.id });
|
||||
let f3 = await Folder.save({ title: 'folder3', parent_id: f2.id });
|
||||
let f4 = await Folder.save({ title: 'folder4' });
|
||||
|
||||
let n1 = await Note.save({ title: 'note1', parent_id: f3.id });
|
||||
let n2 = await Note.save({ title: 'note2', parent_id: f3.id });
|
||||
let n3 = await Note.save({ title: 'note3', parent_id: f1.id });
|
||||
let n4 = await Note.save({ title: 'note4', parent_id: f3.id, is_todo: true, todo_completed: 0 });
|
||||
let n5 = await Note.save({ title: 'note5', parent_id: f3.id, is_todo: true, todo_completed: 999 });
|
||||
let n6 = await Note.save({ title: 'note6', parent_id: f3.id, is_todo: true, todo_completed: 999 });
|
||||
|
||||
const folders = await Folder.all();
|
||||
await Folder.addNoteCounts(folders, false);
|
||||
|
||||
const foldersById = {};
|
||||
folders.forEach((f) => { foldersById[f.id] = f; });
|
||||
|
||||
expect(folders.length).toBe(4);
|
||||
expect(foldersById[f1.id].note_count).toBe(4);
|
||||
expect(foldersById[f2.id].note_count).toBe(3);
|
||||
expect(foldersById[f3.id].note_count).toBe(3);
|
||||
expect(foldersById[f4.id].note_count).toBe(0);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -90,13 +90,13 @@ describe('models_Note', function() {
|
||||
}));
|
||||
|
||||
it('should serialize and unserialize without modifying data', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: 'folder1'});
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
const testCases = [
|
||||
[ {title: '', body: 'Body and no title\nSecond line\nThird Line', parent_id: folder1.id},
|
||||
[{ title: '', body: 'Body and no title\nSecond line\nThird Line', parent_id: folder1.id },
|
||||
'', 'Body and no title\nSecond line\nThird Line'],
|
||||
[ {title: 'Note title', body: 'Body and title', parent_id: folder1.id},
|
||||
[{ title: 'Note title', body: 'Body and title', parent_id: folder1.id },
|
||||
'Note title', 'Body and title'],
|
||||
[ {title: 'Title and no body', body: '', parent_id: folder1.id},
|
||||
[{ title: 'Title and no body', body: '', parent_id: folder1.id },
|
||||
'Title and no body', ''],
|
||||
];
|
||||
|
||||
@@ -116,4 +116,17 @@ describe('models_Note', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
it('should reset fields for a duplicate', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note', parent_id: folder1.id });
|
||||
|
||||
let duplicatedNote = await Note.duplicate(note1.id);
|
||||
|
||||
expect(duplicatedNote !== note1).toBe(true);
|
||||
expect(duplicatedNote.created_time !== note1.created_time).toBe(true);
|
||||
expect(duplicatedNote.updated_time !== note1.updated_time).toBe(true);
|
||||
expect(duplicatedNote.user_created_time !== note1.user_created_time).toBe(true);
|
||||
expect(duplicatedNote.user_updated_time !== note1.user_updated_time).toBe(true);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -86,7 +86,7 @@ describe('models_Tag', function() {
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
|
||||
let tag = await Tag.save({ title: 'mytag'});
|
||||
let tag = await Tag.save({ title: 'mytag' });
|
||||
await Tag.addNote(tag.id, note1.id);
|
||||
|
||||
let tagWithCount = await Tag.loadWithCount(tag.id);
|
||||
@@ -97,4 +97,57 @@ describe('models_Tag', function() {
|
||||
expect(tagWithCount.note_count).toBe(2);
|
||||
}));
|
||||
|
||||
it('should get common tags for set of notes', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let taga = await Tag.save({ title: 'mytaga' });
|
||||
let tagb = await Tag.save({ title: 'mytagb' });
|
||||
let tagc = await Tag.save({ title: 'mytagc' });
|
||||
let tagd = await Tag.save({ title: 'mytagd' });
|
||||
|
||||
let note0 = await Note.save({ title: 'ma note 0', parent_id: folder1.id });
|
||||
let note1 = await Note.save({ title: 'ma note 1', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'ma note 2', parent_id: folder1.id });
|
||||
let note3 = await Note.save({ title: 'ma note 3', parent_id: folder1.id });
|
||||
|
||||
await Tag.addNote(taga.id, note1.id);
|
||||
|
||||
await Tag.addNote(taga.id, note2.id);
|
||||
await Tag.addNote(tagb.id, note2.id);
|
||||
|
||||
await Tag.addNote(taga.id, note3.id);
|
||||
await Tag.addNote(tagb.id, note3.id);
|
||||
await Tag.addNote(tagc.id, note3.id);
|
||||
|
||||
let commonTags = await Tag.commonTagsByNoteIds(null);
|
||||
expect(commonTags.length).toBe(0);
|
||||
|
||||
commonTags = await Tag.commonTagsByNoteIds(undefined);
|
||||
expect(commonTags.length).toBe(0);
|
||||
|
||||
commonTags = await Tag.commonTagsByNoteIds([]);
|
||||
expect(commonTags.length).toBe(0);
|
||||
|
||||
commonTags = await Tag.commonTagsByNoteIds([note0.id, note1.id, note2.id, note3.id]);
|
||||
let commonTagIds = commonTags.map(t => t.id);
|
||||
expect(commonTagIds.length).toBe(0);
|
||||
|
||||
commonTags = await Tag.commonTagsByNoteIds([note1.id, note2.id, note3.id]);
|
||||
commonTagIds = commonTags.map(t => t.id);
|
||||
expect(commonTagIds.length).toBe(1);
|
||||
expect(commonTagIds.includes(taga.id)).toBe(true);
|
||||
|
||||
commonTags = await Tag.commonTagsByNoteIds([note2.id, note3.id]);
|
||||
commonTagIds = commonTags.map(t => t.id);
|
||||
expect(commonTagIds.length).toBe(2);
|
||||
expect(commonTagIds.includes(taga.id)).toBe(true);
|
||||
expect(commonTagIds.includes(tagb.id)).toBe(true);
|
||||
|
||||
commonTags = await Tag.commonTagsByNoteIds([note3.id]);
|
||||
commonTagIds = commonTags.map(t => t.id);
|
||||
expect(commonTags.length).toBe(3);
|
||||
expect(commonTagIds.includes(taga.id)).toBe(true);
|
||||
expect(commonTagIds.includes(tagb.id)).toBe(true);
|
||||
expect(commonTagIds.includes(tagc.id)).toBe(true);
|
||||
}));
|
||||
|
||||
});
|
||||
|
375
CliClient/tests/reducer.js
Normal file
@@ -0,0 +1,375 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
const { setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { reducer, defaultState, stateUtils } = require('lib/reducer.js');
|
||||
|
||||
async function createNTestFolders(n) {
|
||||
let folders = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let folder = await Folder.save({ title: 'folder' });
|
||||
folders.push(folder);
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
async function createNTestNotes(n, folder) {
|
||||
let notes = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let note = await Note.save({ title: 'note', parent_id: folder.id, is_conflict: 0 });
|
||||
notes.push(note);
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function createNTestTags(n) {
|
||||
let tags = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
let tag = await Tag.save({ title: 'tag' });
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tags=null, selectedTagIndex=null) {
|
||||
let state = defaultState;
|
||||
|
||||
if (selectedFolderIndex != null) {
|
||||
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[selectedFolderIndex].id });
|
||||
}
|
||||
if (folders != null) {
|
||||
state = reducer(state, { type: 'FOLDER_UPDATE_ALL', items: folders });
|
||||
}
|
||||
if (notes != null) {
|
||||
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' });
|
||||
}
|
||||
if (selectedIndexes != null) {
|
||||
let selectedIds = [];
|
||||
for (let i = 0; i < selectedIndexes.length; i++) {
|
||||
selectedIds.push(notes[selectedIndexes[i]].id);
|
||||
}
|
||||
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
|
||||
}
|
||||
if (tags != null) {
|
||||
state = reducer(state, { type: 'TAG_UPDATE_ALL', items: tags });
|
||||
}
|
||||
if (selectedTagIndex != null) {
|
||||
state = reducer(state, { type: 'TAG_SELECT', id: tags[selectedTagIndex].id });
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createExpectedState(items, keepIndexes, selectedIndexes) {
|
||||
let expected = { items: [], selectedIds: [] };
|
||||
|
||||
for (let i = 0; i < selectedIndexes.length; i++) {
|
||||
expected.selectedIds.push(items[selectedIndexes[i]].id);
|
||||
}
|
||||
for (let i = 0; i < keepIndexes.length; i++) {
|
||||
expected.items.push(items[keepIndexes[i]]);
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
function getIds(items, indexes=null) {
|
||||
let ids = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (indexes == null || i in indexes) {
|
||||
ids.push(items[i].id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
describe('Reducer', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
insideBeforeEach = true;
|
||||
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
done();
|
||||
|
||||
insideBeforeEach = false;
|
||||
});
|
||||
|
||||
// tests for NOTE_DELETE
|
||||
it('should delete selected note', asyncTest(async () => {
|
||||
// create 1 folder
|
||||
let folders = await createNTestFolders(1);
|
||||
// create 5 notes
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
// select the 1st folder and the 3rd note
|
||||
let state = initTestState(folders, 0, notes, [2]);
|
||||
|
||||
// test action
|
||||
// delete the third note
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
||||
|
||||
// expect that the third note is missing, and the 4th note is now selected
|
||||
let expected = createExpectedState(notes, [0,1,3,4], [3]);
|
||||
|
||||
// check the ids of all the remaining notes
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
// check the ids of the selected notes
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected note at top', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
||||
|
||||
let expected = createExpectedState(notes, [1,2,3,4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete last remaining note', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(1, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [0]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
||||
|
||||
let expected = createExpectedState(notes, [], []);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected note at bottom', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,3], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a note below is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
||||
|
||||
let expected = createExpectedState(notes, [0,2,3,4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a note above is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,4], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete selected notes', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
||||
|
||||
let expected = createExpectedState(notes, [0,3,4], [3]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a notes below it are selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
||||
|
||||
let expected = createExpectedState(notes, [0,2,3,4], [3,4]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete note when a notes above it are selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [1,2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2,4], [1,2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete notes at end', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [3,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2], [2]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
it('should delete notes when non-contiguous selection', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(1);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 0, notes, [0,2,4]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
||||
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
||||
|
||||
let expected = createExpectedState(notes, [1,3], [1]);
|
||||
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
// tests for FOLDER_DELETE
|
||||
it('should delete selected notebook', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 2, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [3]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete notebook when a book above is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 1, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [1]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete notebook when a book below is selected', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(5);
|
||||
let notes = await createNTestNotes(5, folders[0]);
|
||||
let state = initTestState(folders, 4, notes, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
||||
|
||||
let expected = createExpectedState(folders, [0,1,3,4], [4]);
|
||||
|
||||
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
// tests for TAG_DELETE
|
||||
it('should delete selected tag', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'TAG_DELETE', id: tags[2].id });
|
||||
|
||||
let expected = createExpectedState(tags, [0,1,3,4], [3]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete tag when a tag above is selected', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'TAG_DELETE', id: tags[4].id });
|
||||
|
||||
let expected = createExpectedState(tags, [0,1,2,3], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should delete tag when a tag below is selected', asyncTest(async () => {
|
||||
let tags = await createNTestTags(5);
|
||||
let state = initTestState(null, null, null, null, tags, [2]);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'TAG_DELETE', id: tags[0].id });
|
||||
|
||||
let expected = createExpectedState(tags, [1,2,3,4], [2]);
|
||||
|
||||
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
||||
}));
|
||||
|
||||
it('should select all notes', asyncTest(async () => {
|
||||
let folders = await createNTestFolders(2);
|
||||
let notes = [];
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
notes.push(...await createNTestNotes(3, folders[i]));
|
||||
}
|
||||
|
||||
let state = initTestState(folders, 0, notes.slice(0,3), [0]);
|
||||
|
||||
let expected = createExpectedState(notes, [0,1,2], [0]);
|
||||
|
||||
expect(state.notes.length).toEqual(expected.items.length);
|
||||
expect(getIds(state.notes.slice(0,4))).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
|
||||
// test action
|
||||
state = reducer(state, { type: 'NOTE_SELECT_ALL' });
|
||||
|
||||
expected = createExpectedState(notes.slice(0,3), [0,1,2], [0,1,2]);
|
||||
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
||||
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
||||
}));
|
||||
|
||||
});
|
@@ -334,6 +334,35 @@ describe('services_InteropService', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
it('should export selected notes in md format', asyncTest(async () => {
|
||||
const service = new InteropService();
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note11 = await Note.save({ title: 'title note11', parent_id: folder1.id });
|
||||
note11 = await Note.load(note11.id);
|
||||
let note12 = await Note.save({ title: 'title note12', parent_id: folder1.id });
|
||||
note12 = await Note.load(note12.id);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
folder2 = await Folder.load(folder2.id);
|
||||
let note21 = await Note.save({ title: 'title note21', parent_id: folder2.id });
|
||||
note21 = await Note.load(note21.id);
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3', parent_id: folder1.id });
|
||||
folder3 = await Folder.load(folder2.id);
|
||||
|
||||
const outDir = exportDir();
|
||||
|
||||
await service.export({ path: outDir, format: 'md', sourceNoteIds: [note11.id, note21.id] });
|
||||
|
||||
// verify that the md files exist
|
||||
expect(await shim.fsDriver().exists(`${outDir}/folder1`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${outDir}/folder1/title note11.md`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${outDir}/folder1/title note12.md`)).toBe(false);
|
||||
expect(await shim.fsDriver().exists(`${outDir}/folder1/folder2`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${outDir}/folder1/folder2/title note21.md`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${outDir}/folder3`)).toBe(false);
|
||||
}));
|
||||
|
||||
it('should export MD with unicode filenames', asyncTest(async () => {
|
||||
const service = new InteropService();
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
|
364
CliClient/tests/services_InteropService_Exporter_Md.js
Normal file
@@ -0,0 +1,364 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js');
|
||||
const InteropService_Exporter_Md = require('lib/services/InteropService_Exporter_Md.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
||||
|
||||
const exportDir = `${__dirname}/export`;
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('services_InteropService_Exporter_Md', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
await fs.remove(exportDir);
|
||||
await fs.mkdirp(exportDir);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create resources directory', asyncTest(async () => {
|
||||
const service = new InteropService_Exporter_Md();
|
||||
await service.init(exportDir);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should create note paths and add them to context', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note3, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note3 = await Note.load(note3.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note3.body))[0]);
|
||||
|
||||
expect(!exporter.context() && !(exporter.context().notePaths || Object.keys(exporter.context().notePaths).length)).toBe(false, 'Context should be empty before processing.');
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(3, 'There should be 3 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note2.id]).toBe('folder1/note2.md');
|
||||
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.md');
|
||||
}));
|
||||
|
||||
it('should handle duplicate note names', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
let note1_2 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1_2);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(2, 'There should be 2 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1 (1).md');
|
||||
}));
|
||||
|
||||
it('should not override existing files', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
// Create a file with the path of note1 before processing note1
|
||||
await shim.fsDriver().writeFile(`${exportDir}/folder1/note1.md`, 'Note content', 'utf-8');
|
||||
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(1, 'There should be 1 note paths in the context.');
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1 (1).md');
|
||||
}));
|
||||
|
||||
it('should save resource files in _resource directory', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note1.body))[0]);
|
||||
let resource1 = await Resource.load(itemsToExport[2].itemOrId);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note2 = await Note.load(note2.id);
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note2.body))[0]);
|
||||
let resource2 = await Resource.load(itemsToExport[5].itemOrId);
|
||||
|
||||
await exporter.processResource(resource1, Resource.fullPath(resource1));
|
||||
await exporter.processResource(resource2, Resource.fullPath(resource2));
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource1)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/_resources/${Resource.filename(resource2)}`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
}));
|
||||
|
||||
it('should create folders in fs', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder3.id);
|
||||
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.processItem(Folder, folder3);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note2);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/folder1`)).toBe(true, 'Folder should be created in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/folder1/folder2`)).toBe(true, 'Folder should be created in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/folder1/folder3`)).toBe(true, 'Folder should be created in filesystem.');
|
||||
}));
|
||||
|
||||
it('should save notes in fs', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
|
||||
queueExportItem(BaseModel.TYPE_FOLDER, folder3.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
await exporter.processItem(Note, note3);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note1.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note2.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir}/${exporter.context().notePaths[note3.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
}));
|
||||
|
||||
it('should replace resource ids with relative paths', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note1 = await Note.load(note1.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
let resource1 = await Resource.load((await Note.linkedResourceIds(note1.body))[0]);
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
|
||||
note2 = await Note.load(note2.id);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
let resource2 = await Resource.load((await Note.linkedResourceIds(note2.body))[0]);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
let context = {
|
||||
resourcePaths: {},
|
||||
};
|
||||
context.resourcePaths[resource1.id] = 'resource1.jpg';
|
||||
context.resourcePaths[resource2.id] = 'resource2.jpg';
|
||||
exporter.updateContext(context);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
|
||||
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../_resources/resource1.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../_resources/resource2.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
}));
|
||||
|
||||
it('should replace note ids with relative paths', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
const changeNoteBodyAndReload = async (note, newBody) => {
|
||||
note.body = newBody;
|
||||
await Note.save(note);
|
||||
return await Note.load(note.id);
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'note1', parent_id: folder1.id });
|
||||
|
||||
let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder2.id });
|
||||
|
||||
let folder3 = await Folder.save({ title: 'folder3' });
|
||||
let note3 = await Note.save({ title: 'note3', parent_id: folder3.id });
|
||||
|
||||
note1 = await changeNoteBodyAndReload(note1, `# Some text \n\n [A link to note3](:/${note3.id})`);
|
||||
note2 = await changeNoteBodyAndReload(note2, `# Some text \n\n [A link to note3](:/${note3.id}) some more text \n ## And some headers \n and [A link to note1](:/${note1.id}) more links`);
|
||||
note3 = await changeNoteBodyAndReload(note3, `[A link to note3](:/${note2.id})`);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.processItem(Folder, folder2);
|
||||
await exporter.processItem(Folder, folder3);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
await exporter.processItem(Note, note3);
|
||||
|
||||
let note1_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note1.id]}`);
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
let note3_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note3.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../folder3/note3.md)', 'Note id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder3/note3.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder1/note1.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note3_body).toContain('](../folder1/folder2/note2.md)', 'Resource id should be replaced with a relative path.');
|
||||
}));
|
||||
|
||||
it('should url encode relative note links', asyncTest(async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir);
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder with space1' });
|
||||
let note1 = await Note.save({ title: 'note1 name with space', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'note2', parent_id: folder1.id, body: `[link](:/${note1.id})` });
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note1);
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note2);
|
||||
|
||||
await exporter.processItem(Folder, folder1);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note, note1);
|
||||
await exporter.processItem(Note, note2);
|
||||
|
||||
let note2_body = await shim.fsDriver().readFile(`${exportDir}/${exporter.context().notePaths[note2.id]}`);
|
||||
expect(note2_body).toContain('[link](../folder%20with%20space1/note1%20name%20with%20space.md)', 'Whitespace in URL should be encoded');
|
||||
}));
|
||||
});
|
@@ -258,6 +258,16 @@ describe('services_SearchEngine', function() {
|
||||
expect((await engine.search('말')).length).toBe(1);
|
||||
}));
|
||||
|
||||
it('should support queries with Thai characters', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'นี่คือคนไทย' });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
expect((await engine.search('นี่คือค')).length).toBe(1);
|
||||
expect((await engine.search('ไทย')).length).toBe(1);
|
||||
}));
|
||||
|
||||
it('should support field restricted queries with Chinese characters', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: '你好', body: '我是法国人' });
|
||||
|
@@ -279,7 +279,7 @@ describe('services_rest_Api', function() {
|
||||
const response = await api.route('GET', 'notes', { token: 'mytoken' });
|
||||
expect(response.length).toBe(0);
|
||||
|
||||
hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({title: 'testing'})));
|
||||
hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing' })));
|
||||
expect(hasThrown).toBe(true);
|
||||
}));
|
||||
|
||||
|
BIN
CliClient/tests/support/photo-two.jpg
Normal file
After Width: | Height: | Size: 2.7 KiB |
@@ -539,7 +539,7 @@ describe('Synchronizer', function() {
|
||||
let context2 = await synchronizer().start();
|
||||
if (withEncryption) {
|
||||
const masterKey_2 = await MasterKey.load(masterKey.id);
|
||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
|
||||
let t = await Tag.load(tag.id);
|
||||
await Tag.decrypt(t);
|
||||
}
|
||||
@@ -743,7 +743,7 @@ describe('Synchronizer', function() {
|
||||
expect(masterKey_2.content).toBe(masterKey.content);
|
||||
expect(masterKey_2.checksum).toBe(masterKey.checksum);
|
||||
// Now load the master key we got from client 1 and try to decrypt
|
||||
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||
await encryptionService().loadMasterKey_(masterKey_2, '123456', true);
|
||||
// Get the decrypted items back
|
||||
await Folder.decrypt(folder1_2);
|
||||
await Note.decrypt(note1_2);
|
||||
@@ -1540,5 +1540,43 @@ describe('Synchronizer', function() {
|
||||
expect((await synchronizer().lockFiles_()).length).toBe(0);
|
||||
}));
|
||||
|
||||
it('should not encrypt notes that are shared', asyncTest(async () => {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
await loadEncryptionMasterKey();
|
||||
|
||||
let folder1 = await Folder.save({ title: 'folder1' });
|
||||
let note1 = await Note.save({ title: 'un', parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: 'deux', parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
const origNote2 = Object.assign({}, note2);
|
||||
await BaseItem.updateShareStatus(note2, true);
|
||||
note2 = await Note.load(note2.id);
|
||||
|
||||
// Sharing a note should not modify the timestamps
|
||||
expect(note2.user_updated_time).toBe(origNote2.user_updated_time);
|
||||
expect(note2.user_created_time).toBe(origNote2.user_created_time);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
// The shared note should be decrypted
|
||||
let note2_2 = await Note.load(note2.id);
|
||||
expect(note2_2.title).toBe('deux');
|
||||
expect(note2_2.is_shared).toBe(1);
|
||||
|
||||
// The non-shared note should be encrypted
|
||||
let note1_2 = await Note.load(note1.id);
|
||||
expect(note1_2.title).toBe('');
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -299,7 +299,7 @@ async function loadEncryptionMasterKey(id = null, useExisting = false) {
|
||||
masterKey = masterKeys[0];
|
||||
}
|
||||
|
||||
await service.loadMasterKey(masterKey, '123456', true);
|
||||
await service.loadMasterKey_(masterKey, '123456', true);
|
||||
|
||||
return masterKey;
|
||||
}
|
||||
@@ -370,8 +370,12 @@ function asyncTest(callback) {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
expect('good').toBe('not good', 'Test has thrown an exception - see above error');
|
||||
if (error.constructor && error.constructor.name === 'ExpectationFailed') {
|
||||
// OK - will be reported by Jasmine
|
||||
} else {
|
||||
console.error(error);
|
||||
expect(0).toBe(1, 'Test has thrown an exception - see above error');
|
||||
}
|
||||
} finally {
|
||||
done();
|
||||
}
|
||||
|
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import './App.css';
|
||||
import led_red from './led_red.png';
|
||||
@@ -187,10 +189,10 @@ class AppComponent extends Component {
|
||||
}
|
||||
|
||||
async loadContentScripts() {
|
||||
await bridge().tabsExecuteScript({file: '/content_scripts/JSDOMParser.js'});
|
||||
await bridge().tabsExecuteScript({file: '/content_scripts/Readability.js'});
|
||||
await bridge().tabsExecuteScript({file: '/content_scripts/Readability-readerable.js'});
|
||||
await bridge().tabsExecuteScript({file: '/content_scripts/index.js'});
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
@@ -246,7 +248,7 @@ class AppComponent extends Component {
|
||||
if (!this.state.contentScriptLoaded) {
|
||||
let msg = 'Loading...';
|
||||
if (this.state.contentScriptError) msg = `The Joplin extension is not available on this tab due to: ${this.state.contentScriptError}`;
|
||||
return <div style={{padding: 10, fontSize: 12, maxWidth: 200}}>{msg}</div>;
|
||||
return <div style={{ padding: 10, fontSize: 12, maxWidth: 200 }}>{msg}</div>;
|
||||
}
|
||||
|
||||
const warningComponent = !this.props.warning ? null : <div className="Warning">{ this.props.warning }</div>;
|
||||
|
@@ -33,7 +33,14 @@ class ElectronAppWrapper {
|
||||
return this.win_;
|
||||
}
|
||||
|
||||
env() {
|
||||
return this.env_;
|
||||
}
|
||||
|
||||
createWindow() {
|
||||
// Set to true to view errors if the application does not start
|
||||
const debugEarlyBugs = this.env_ === 'dev' && false;
|
||||
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
|
||||
const stateOptions = {
|
||||
@@ -56,9 +63,10 @@ class ElectronAppWrapper {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
webviewTag: true,
|
||||
// We start with a hidden window, which is then made visible depending on the showTrayIcon setting
|
||||
// https://github.com/laurent22/joplin/issues/2031
|
||||
show: false,
|
||||
show: debugEarlyBugs,
|
||||
};
|
||||
|
||||
// Linux icon workaround for bug https://github.com/electron-userland/electron-builder/issues/2098
|
||||
@@ -81,8 +89,11 @@ class ElectronAppWrapper {
|
||||
slashes: true,
|
||||
}));
|
||||
|
||||
// Uncomment this to view errors if the application does not start
|
||||
// if (this.env_ === 'dev') this.win_.webContents.openDevTools();
|
||||
// Note that on Windows, calling openDevTools() too early results in a white window with no error message.
|
||||
// Waiting for one of the ready events might work but they might not be triggered if there's an error, so
|
||||
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
|
||||
// to this line though.
|
||||
if (debugEarlyBugs) setTimeout(() => this.win_.webContents.openDevTools(), 3000);
|
||||
|
||||
this.win_.on('close', (event) => {
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
@@ -113,6 +124,13 @@ class ElectronAppWrapper {
|
||||
// automatically (the listeners will be removed when the window is closed)
|
||||
// and restore the maximized or full screen state
|
||||
windowState.manage(this.win_);
|
||||
|
||||
// HACK: Ensure the window is hidden, as `windowState.manage` may make the window
|
||||
// visible with isMaximized set to true in window-state-${this.env_}.json.
|
||||
// https://github.com/laurent22/joplin/issues/2365
|
||||
if (!windowOptions.show) {
|
||||
this.win_.hide();
|
||||
}
|
||||
}
|
||||
|
||||
async waitForElectronAppReady() {
|
||||
@@ -176,7 +194,7 @@ class ElectronAppWrapper {
|
||||
createTray(contextMenu) {
|
||||
try {
|
||||
this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`);
|
||||
this.tray_.setToolTip(this.electronApp_.getName());
|
||||
this.tray_.setToolTip(this.electronApp_.name);
|
||||
this.tray_.setContextMenu(contextMenu);
|
||||
|
||||
this.tray_.on('click', () => {
|
||||
|
@@ -1,9 +1,97 @@
|
||||
const { _ } = require('lib/locale');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const InteropService = require('lib/services/InteropService');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const md5 = require('md5');
|
||||
const url = require('url');
|
||||
const { shim } = require('lib/shim');
|
||||
|
||||
class InteropServiceHelper {
|
||||
|
||||
static async exportNoteToHtmlFile(noteId, exportOptions) {
|
||||
const tempFile = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}.html`;
|
||||
|
||||
exportOptions = Object.assign({}, {
|
||||
path: tempFile,
|
||||
format: 'html',
|
||||
target: 'file',
|
||||
sourceNoteIds: [noteId],
|
||||
customCss: '',
|
||||
}, exportOptions);
|
||||
|
||||
const service = new InteropService();
|
||||
|
||||
const result = await service.export(exportOptions);
|
||||
console.info('Export HTML result: ', result);
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
static async exportNoteTo_(target, noteId, options = {}) {
|
||||
let win = null;
|
||||
let htmlFile = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (win) win.destroy();
|
||||
if (htmlFile) shim.fsDriver().remove(htmlFile);
|
||||
};
|
||||
|
||||
try {
|
||||
const exportOptions = {
|
||||
customCss: options.customCss ? options.customCss : '',
|
||||
};
|
||||
|
||||
htmlFile = await this.exportNoteToHtmlFile(noteId, exportOptions);
|
||||
|
||||
const windowOptions = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
win = bridge().newBrowserWindow(windowOptions);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
win.webContents.on('did-finish-load', async () => {
|
||||
|
||||
if (target === 'pdf') {
|
||||
try {
|
||||
const data = await win.webContents.printToPDF(options);
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
} else {
|
||||
win.webContents.print(options, (success, reason) => {
|
||||
// TODO: This is correct but broken in Electron 4. Need to upgrade to 5+
|
||||
// It calls the callback right away with "false" even if the document hasn't be print yet.
|
||||
|
||||
cleanup();
|
||||
if (!success && reason !== 'cancelled') reject(new Error(`Could not print: ${reason}`));
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
win.loadURL(url.format({
|
||||
pathname: htmlFile,
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async exportNoteToPdf(noteId, options = {}) {
|
||||
return this.exportNoteTo_('pdf', noteId, options);
|
||||
}
|
||||
|
||||
static async printNote(noteId, options = {}) {
|
||||
return this.exportNoteTo_('printer', noteId, options);
|
||||
}
|
||||
|
||||
static async export(dispatch, module, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
@@ -11,7 +99,7 @@ class InteropServiceHelper {
|
||||
|
||||
if (module.target === 'file') {
|
||||
path = bridge().showSaveDialog({
|
||||
filters: [{ name: module.description, extensions: module.fileExtensions}],
|
||||
filters: [{ name: module.description, extensions: module.fileExtensions }],
|
||||
});
|
||||
} else {
|
||||
path = bridge().showOpenDialog({
|
||||
@@ -32,6 +120,8 @@ class InteropServiceHelper {
|
||||
const exportOptions = {};
|
||||
exportOptions.path = path;
|
||||
exportOptions.format = module.format;
|
||||
exportOptions.modulePath = module.path;
|
||||
exportOptions.target = module.target;
|
||||
if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds;
|
||||
if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds;
|
||||
|
||||
@@ -41,6 +131,7 @@ class InteropServiceHelper {
|
||||
const result = await service.export(exportOptions);
|
||||
console.info('Export result: ', result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
bridge().showErrorMessageBox(_('Could not export notes: %s', error.message));
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ const Setting = require('lib/models/Setting.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const Note = require('lib/models/Note');
|
||||
const { MarkupToHtml } = require('lib/joplin-renderer');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const fs = require('fs-extra');
|
||||
@@ -28,6 +29,7 @@ const PluginManager = require('lib/services/PluginManager');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const MigrationService = require('lib/services/MigrationService');
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const CssUtils = require('lib/CssUtils');
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything.min'),
|
||||
@@ -48,7 +50,7 @@ const appDefaultState = Object.assign({}, defaultState, {
|
||||
windowContentSize: bridge().windowContentSize(),
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
noteDevToolsVisible: false,
|
||||
devToolsVisible: false,
|
||||
});
|
||||
|
||||
class Application extends BaseApplication {
|
||||
@@ -220,7 +222,12 @@ class Application extends BaseApplication {
|
||||
|
||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||
newState = Object.assign({}, state);
|
||||
newState.noteDevToolsVisible = !newState.noteDevToolsVisible;
|
||||
newState.devToolsVisible = !newState.devToolsVisible;
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_SET':
|
||||
newState = Object.assign({}, state);
|
||||
newState.devToolsVisible = action.value;
|
||||
break;
|
||||
|
||||
}
|
||||
@@ -232,6 +239,14 @@ class Application extends BaseApplication {
|
||||
return super.reducer(newState, action);
|
||||
}
|
||||
|
||||
toggleDevTools(visible) {
|
||||
if (visible) {
|
||||
bridge().openDevTools();
|
||||
} else {
|
||||
bridge().closeDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
async generalMiddleware(store, next, action) {
|
||||
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
|
||||
setLocale(Setting.value('locale'));
|
||||
@@ -273,12 +288,12 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
if (action.type.indexOf('NOTE_SELECT') === 0 || action.type.indexOf('FOLDER_SELECT') === 0) {
|
||||
this.updateMenuItemStates();
|
||||
this.updateMenuItemStates(newState);
|
||||
}
|
||||
|
||||
if (action.type === 'NOTE_DEVTOOLS_TOGGLE') {
|
||||
const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
|
||||
menuItem.checked = newState.noteDevToolsVisible;
|
||||
if (['NOTE_DEVTOOLS_TOGGLE', 'NOTE_DEVTOOLS_SET'].indexOf(action.type) >= 0) {
|
||||
this.toggleDevTools(newState.devToolsVisible);
|
||||
this.updateMenuItemStates(newState);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -371,13 +386,15 @@ class Application extends BaseApplication {
|
||||
for (let i = 0; i < ioModules.length; i++) {
|
||||
const module = ioModules[i];
|
||||
if (module.type === 'exporter') {
|
||||
exportItems.push({
|
||||
label: module.fullLabel(),
|
||||
screens: ['Main'],
|
||||
click: async () => {
|
||||
await InteropServiceHelper.export(this.dispatch.bind(this), module);
|
||||
},
|
||||
});
|
||||
if (module.canDoMultiExport !== false) {
|
||||
exportItems.push({
|
||||
label: module.fullLabel(),
|
||||
screens: ['Main'],
|
||||
click: async () => {
|
||||
await InteropServiceHelper.export(this.dispatch.bind(this), module);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (let j = 0; j < module.sources.length; j++) {
|
||||
const moduleSource = module.sources[j];
|
||||
@@ -391,7 +408,7 @@ class Application extends BaseApplication {
|
||||
|
||||
if (moduleSource === 'file') {
|
||||
path = bridge().showOpenDialog({
|
||||
filters: [{ name: module.description, extensions: module.fileExtensions}],
|
||||
filters: [{ name: module.description, extensions: module.fileExtensions }],
|
||||
});
|
||||
} else {
|
||||
path = bridge().showOpenDialog({
|
||||
@@ -445,6 +462,7 @@ class Application extends BaseApplication {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'exportPdf',
|
||||
noteId: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -611,6 +629,7 @@ class Application extends BaseApplication {
|
||||
'',
|
||||
_('Client ID: %s', Setting.value('clientId')),
|
||||
_('Sync Version: %s', Setting.value('syncVersion')),
|
||||
_('Profile Version: %s', reg.db().version()),
|
||||
];
|
||||
if (gitInfo) {
|
||||
message.push(`\n${gitInfo}`);
|
||||
@@ -855,12 +874,10 @@ class Application extends BaseApplication {
|
||||
accelerator: 'CommandOrControl+Alt+T',
|
||||
click: () => {
|
||||
const selectedNoteIds = this.store().getState().selectedNoteIds;
|
||||
if (selectedNoteIds.length !== 1) return;
|
||||
|
||||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: selectedNoteIds[0],
|
||||
noteIds: selectedNoteIds,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
@@ -1104,18 +1121,23 @@ class Application extends BaseApplication {
|
||||
this.lastMenuScreen_ = screen;
|
||||
}
|
||||
|
||||
async updateMenuItemStates() {
|
||||
async updateMenuItemStates(state = null) {
|
||||
if (!this.lastMenuScreen_) return;
|
||||
if (!this.store()) return;
|
||||
if (!this.store() && !state) return;
|
||||
|
||||
const selectedNoteIds = this.store().getState().selectedNoteIds;
|
||||
if (!state) state = this.store().getState();
|
||||
|
||||
const selectedNoteIds = state.selectedNoteIds;
|
||||
const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null;
|
||||
|
||||
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'setTags', 'showLocalSearch']) {
|
||||
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'showLocalSearch']) {
|
||||
const menuItem = Menu.getApplicationMenu().getMenuItemById(`edit:${itemId}`);
|
||||
if (!menuItem) continue;
|
||||
menuItem.enabled = !!note && note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN;
|
||||
menuItem.enabled = !!note && note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
}
|
||||
|
||||
const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
|
||||
menuItem.checked = state.devToolsVisible;
|
||||
}
|
||||
|
||||
updateTray() {
|
||||
@@ -1127,7 +1149,7 @@ class Application extends BaseApplication {
|
||||
app.destroyTray();
|
||||
} else {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } },
|
||||
{ label: _('Open %s', app.electronApp().name), click: () => { app.window().show(); } },
|
||||
{ type: 'separator' },
|
||||
{ label: _('Exit'), click: () => { app.quit(); } },
|
||||
]);
|
||||
@@ -1167,6 +1189,39 @@ class Application extends BaseApplication {
|
||||
return cssString;
|
||||
}
|
||||
|
||||
// async createManyNotes() {
|
||||
// return;
|
||||
// const folderIds = [];
|
||||
|
||||
// const randomFolderId = (folderIds) => {
|
||||
// if (!folderIds.length) return '';
|
||||
// const idx = Math.floor(Math.random() * folderIds.length);
|
||||
// if (idx > folderIds.length - 1) throw new Error('Invalid index ' + idx + ' / ' + folderIds.length);
|
||||
// return folderIds[idx];
|
||||
// }
|
||||
|
||||
// let rootFolderCount = 0;
|
||||
// let folderCount = 100;
|
||||
|
||||
// for (let i = 0; i < folderCount; i++) {
|
||||
// let parentId = '';
|
||||
|
||||
// if (Math.random() >= 0.9 || rootFolderCount >= folderCount / 10) {
|
||||
// parentId = randomFolderId(folderIds);
|
||||
// } else {
|
||||
// rootFolderCount++;
|
||||
// }
|
||||
|
||||
// const folder = await Folder.save({ title: 'folder' + i, parent_id: parentId });
|
||||
// folderIds.push(folder.id);
|
||||
// }
|
||||
|
||||
// for (let i = 0; i < 10000; i++) {
|
||||
// const parentId = randomFolderId(folderIds);
|
||||
// Note.save({ title: 'note' + i, parent_id: parentId });
|
||||
// }
|
||||
// }
|
||||
|
||||
async start(argv) {
|
||||
const electronIsDev = require('electron-is-dev');
|
||||
|
||||
@@ -1176,13 +1231,18 @@ class Application extends BaseApplication {
|
||||
|
||||
argv = await super.start(argv);
|
||||
|
||||
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
||||
const dir = Setting.value('profileDir');
|
||||
const filename = Setting.custom_css_files.JOPLIN_APP;
|
||||
await CssUtils.injectCustomStyles(`${dir}/${filename}`);
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
|
||||
reg.setShowErrorMessageBoxHandler((message) => { bridge().showErrorMessageBox(message); });
|
||||
|
||||
if (Setting.value('openDevTools')) {
|
||||
bridge().window().webContents.openDevTools();
|
||||
if (Setting.value('flagOpenDevTools')) {
|
||||
bridge().openDevTools();
|
||||
}
|
||||
|
||||
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||
@@ -1224,8 +1284,8 @@ class Application extends BaseApplication {
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
const cssString = await this.loadCustomCss(`${Setting.value('profileDir')}/userstyle.css`);
|
||||
|
||||
// Loads custom Markdown preview styles
|
||||
const cssString = await CssUtils.loadCustomCss(`${Setting.value('profileDir')}/userstyle.css`);
|
||||
this.store().dispatch({
|
||||
type: 'LOAD_CUSTOM_CSS',
|
||||
css: cssString,
|
||||
@@ -1238,6 +1298,11 @@ class Application extends BaseApplication {
|
||||
templates: templates,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_DEVTOOLS_SET',
|
||||
value: Setting.value('flagOpenDevTools'),
|
||||
});
|
||||
|
||||
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
||||
// but then doesn't install it on exit.
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { dirname } = require('lib/path-utils.js');
|
||||
const { BrowserWindow } = require('electron');
|
||||
|
||||
class Bridge {
|
||||
|
||||
@@ -13,6 +14,10 @@ class Bridge {
|
||||
return this.electronWrapper_;
|
||||
}
|
||||
|
||||
env() {
|
||||
return this.electronWrapper_.env();
|
||||
}
|
||||
|
||||
processArgv() {
|
||||
return process.argv;
|
||||
}
|
||||
@@ -21,6 +26,10 @@ class Bridge {
|
||||
return this.electronWrapper_.window();
|
||||
}
|
||||
|
||||
newBrowserWindow(options) {
|
||||
return new BrowserWindow(options);
|
||||
}
|
||||
|
||||
windowContentSize() {
|
||||
if (!this.window()) return { width: 0, height: 0 };
|
||||
const s = this.window().getContentSize();
|
||||
@@ -38,11 +47,19 @@ class Bridge {
|
||||
return this.window().setSize(width, height);
|
||||
}
|
||||
|
||||
openDevTools() {
|
||||
return this.window().webContents.openDevTools();
|
||||
}
|
||||
|
||||
closeDevTools() {
|
||||
return this.window().webContents.closeDevTools();
|
||||
}
|
||||
|
||||
showSaveDialog(options) {
|
||||
const {dialog} = require('electron');
|
||||
const { dialog } = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
const filePath = dialog.showSaveDialog(this.window(), options);
|
||||
const filePath = dialog.showSaveDialogSync(this.window(), options);
|
||||
if (filePath) {
|
||||
this.lastSelectedPath_ = filePath;
|
||||
}
|
||||
@@ -50,11 +67,11 @@ class Bridge {
|
||||
}
|
||||
|
||||
showOpenDialog(options) {
|
||||
const {dialog} = require('electron');
|
||||
const { dialog } = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
const filePaths = dialog.showOpenDialog(this.window(), options);
|
||||
const filePaths = dialog.showOpenDialogSync(this.window(), options);
|
||||
if (filePaths && filePaths.length) {
|
||||
this.lastSelectedPath_ = dirname(filePaths[0]);
|
||||
}
|
||||
@@ -63,15 +80,16 @@ class Bridge {
|
||||
|
||||
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
|
||||
showMessageBox_(window, options) {
|
||||
const {dialog} = require('electron');
|
||||
const { dialog } = require('electron');
|
||||
if (!window) window = this.window();
|
||||
return dialog.showMessageBox(window, options);
|
||||
return dialog.showMessageBoxSync(window, options);
|
||||
}
|
||||
|
||||
showErrorMessageBox(message) {
|
||||
return this.showMessageBox_(this.window(), {
|
||||
type: 'error',
|
||||
message: message,
|
||||
buttons: [_('OK')],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,6 +144,10 @@ class Bridge {
|
||||
return this.electronApp().buildDir();
|
||||
}
|
||||
|
||||
screen() {
|
||||
return require('electron').screen;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let bridge_ = null;
|
||||
|
@@ -126,13 +126,13 @@ function checkForUpdates(inBackground, window, logFilePath, options) {
|
||||
|
||||
autoUpdateLogger_.info(`checkForUpdates: Checking with options ${JSON.stringify(options)}`);
|
||||
|
||||
fetchLatestRelease(options).then(release => {
|
||||
fetchLatestRelease(options).then(async (release) => {
|
||||
autoUpdateLogger_.info(`Current version: ${packageInfo.version}`);
|
||||
autoUpdateLogger_.info(`Latest version: ${release.version}`);
|
||||
autoUpdateLogger_.info('Is Pre-release:', release.prerelease);
|
||||
|
||||
if (compareVersions(release.version, packageInfo.version) <= 0) {
|
||||
if (!checkInBackground_) dialog.showMessageBox({
|
||||
if (!checkInBackground_) await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
message: _('Current version is up-to-date.'),
|
||||
buttons: [_('OK')],
|
||||
@@ -145,12 +145,13 @@ function checkForUpdates(inBackground, window, logFilePath, options) {
|
||||
|
||||
const newVersionString = release.prerelease ? _('%s (pre-release)', release.version) : release.version;
|
||||
|
||||
const buttonIndex = dialog.showMessageBox(parentWindow_, {
|
||||
const result = await dialog.showMessageBox(parentWindow_, {
|
||||
type: 'info',
|
||||
message: `${_('An update is available, do you want to download it now?')}\n\n${_('Your version: %s', packageInfo.version)}\n${_('New version: %s', newVersionString)}${releaseNotes}`,
|
||||
buttons: [_('Yes'), _('No')].concat(truncateReleaseNotes ? [_('Full Release Notes')] : []),
|
||||
});
|
||||
|
||||
const buttonIndex = result.response;
|
||||
if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
|
||||
if (buttonIndex === 2) require('electron').shell.openExternal(release.pageUrl);
|
||||
}
|
||||
|
@@ -20,10 +20,14 @@ packageInfo.build = { appId: appId };
|
||||
let branch;
|
||||
let hash;
|
||||
try {
|
||||
branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
||||
hash = execSync('git log --pretty="%h" -1').toString().trim();
|
||||
// Use stdio: 'pipe' so that execSync doesn't print error directly to stdout
|
||||
branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
|
||||
hash = execSync('git log --pretty="%h" -1', { stdio: 'pipe' }).toString().trim();
|
||||
} catch (err) {
|
||||
console.warn('Could not get git info', err);
|
||||
// Don't display error object as it's a "fatal" error, but
|
||||
// not for us, since is it not critical information
|
||||
// https://github.com/laurent22/joplin/issues/2256
|
||||
console.info('Warning: Could not get git info (it will not be displayed in About dialog box)');
|
||||
}
|
||||
if (typeof branch !== 'undefined' && typeof hash !== 'undefined') {
|
||||
packageInfo.git = { branch: branch, hash: hash };
|
||||
|
18
ElectronClient/app/copyPluginAssets.js
Normal file
@@ -0,0 +1,18 @@
|
||||
require('app-module-path').addPath(`${__dirname}`);
|
||||
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const rootDir = __dirname;
|
||||
const sourceDir = `${rootDir}/../../ReactNativeClient/lib/joplin-renderer/assets`;
|
||||
const destDir = `${rootDir}/gui/note-viewer/pluginAssets`;
|
||||
|
||||
async function main() {
|
||||
await fs.remove(destDir);
|
||||
await fs.mkdirp(destDir);
|
||||
await fs.copy(sourceDir, destDir);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
@@ -119,9 +121,9 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
<div style={stepBoxStyle}>
|
||||
<p style={theme.h1Style}>{_('Step 2: Install the extension')}</p>
|
||||
<p style={theme.textStyle}>{_('Download and install the relevant extension for your browser:')}</p>
|
||||
<div style={{display: 'flex', flexDirection: 'row'}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<ExtensionBadge theme={this.props.theme} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/>
|
||||
<ExtensionBadge style={{marginLeft: 10}} theme={this.props.theme} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
|
||||
<ExtensionBadge style={{ marginLeft: 10 }} theme={this.props.theme} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const styleSelector = require('./style/ConfigMenuBar');
|
||||
const Setting = require('lib/models/Setting');
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
@@ -24,6 +26,19 @@ class ConfigScreenComponent extends React.Component {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
};
|
||||
|
||||
this.checkNextcloudAppButton_click = async () => {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
await shared.checkNextcloudApp(this, this.state.settings);
|
||||
};
|
||||
|
||||
this.showLogButton_click = () => {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
};
|
||||
|
||||
this.nextcloudAppHelpLink_click = () => {
|
||||
bridge().openExternal('https://joplinapp.org/nextcloud_app');
|
||||
};
|
||||
|
||||
this.rowStyle_ = {
|
||||
marginBottom: 10,
|
||||
};
|
||||
@@ -31,7 +46,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
@@ -93,14 +108,21 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
sectionToComponent(key, section, settings, selected) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const settingComps = [];
|
||||
// const settingComps = [];
|
||||
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
const createSettingComponents = (advanced) => {
|
||||
const output = [];
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
if (!!md.advanced !== advanced) continue;
|
||||
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
||||
output.push(settingComp);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
||||
settingComps.push(settingComp);
|
||||
}
|
||||
const settingComps = createSettingComponents(false);
|
||||
const advancedSettingComps = createSettingComponents(true);
|
||||
|
||||
const sectionStyle = {
|
||||
marginTop: 20,
|
||||
@@ -117,10 +139,10 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
if (section.name === 'sync') {
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
const statusComp = !messages.length ? null : (
|
||||
<div style={statusStyle}>
|
||||
{messages[0]}
|
||||
@@ -137,12 +159,69 @@ class ConfigScreenComponent extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (syncTargetMd.name === 'nextcloud') {
|
||||
const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']];
|
||||
|
||||
let status = _('Unknown');
|
||||
let errorMessage = null;
|
||||
|
||||
if (this.state.checkNextcloudAppResult === 'checking') {
|
||||
status = _('Checking...');
|
||||
} else if (syncTarget) {
|
||||
if (syncTarget.uuid) status = _('OK');
|
||||
if (syncTarget.error) {
|
||||
status = _('Error');
|
||||
errorMessage = syncTarget.error;
|
||||
}
|
||||
}
|
||||
|
||||
const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (
|
||||
<div style={statusStyle}>
|
||||
<p style={theme.textStyle}>{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}</p>
|
||||
<pre>{errorMessage}</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
|
||||
<a style={theme.urlStyle} href="#" onClick={this.showLogButton_click}>[{_('Show Log')}]</a>
|
||||
);
|
||||
|
||||
const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });
|
||||
|
||||
settingComps.push(
|
||||
<div key="nextcloud_app_check" style={this.rowStyle_}>
|
||||
<span style={theme.textStyle}>Beta: {_('Joplin Nextcloud App status:')} </span><span style={appStatusStyle}>{status}</span>
|
||||
|
||||
{showLogButton}
|
||||
|
||||
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
|
||||
{_('Check Status')}
|
||||
</button>
|
||||
|
||||
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
|
||||
{statusComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let advancedSettingsButton = null;
|
||||
let advancedSettingsSectionStyle = { display: 'none' };
|
||||
|
||||
if (advancedSettingComps.length) {
|
||||
const iconName = this.state.showAdvancedSettings ? 'fa fa-toggle-up' : 'fa fa-toggle-down';
|
||||
const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
|
||||
advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{ fontSize: 14 }} className={iconName}></i> {_('Show Advanced Settings')}</button>;
|
||||
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} style={sectionStyle}>
|
||||
{noteComp}
|
||||
<div>{settingComps}</div>
|
||||
{advancedSettingsButton}
|
||||
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -413,6 +492,24 @@ class ConfigScreenComponent extends React.Component {
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const buttonStyle = Object.assign({}, theme.buttonStyle, {
|
||||
display: 'inline-block',
|
||||
marginRight: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}>
|
||||
<label>{md.label()}</label>
|
||||
</div>
|
||||
<button style={buttonStyle} onClick={md.onClick}>
|
||||
{_('Edit')}
|
||||
</button>
|
||||
{descriptionComp}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.warn(`Type not implemented: ${key}`);
|
||||
}
|
||||
@@ -478,7 +575,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
borderTopColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const screenComp = this.state.screenName ? <div style={{overflow: 'scroll', flex: 1}}>{this.screenFromName(this.state.screenName)}</div> : null;
|
||||
const screenComp = this.state.screenName ? <div style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null;
|
||||
|
||||
if (screenComp) containerStyle.display = 'none';
|
||||
|
||||
|
47
ElectronClient/app/gui/DialogButtonRow.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
||||
function DialogButtonRow(props) {
|
||||
const theme = themeStyle(props.theme);
|
||||
|
||||
const okButton_click = () => {
|
||||
if (props.onClick) props.onClick({ buttonName: 'ok' });
|
||||
};
|
||||
|
||||
const cancelButton_click = () => {
|
||||
if (props.onClick) props.onClick({ buttonName: 'cancel' });
|
||||
};
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (event.keyCode === 13) {
|
||||
okButton_click();
|
||||
} else if (event.keyCode === 27) {
|
||||
cancelButton_click();
|
||||
}
|
||||
};
|
||||
|
||||
const buttonComps = [];
|
||||
|
||||
if (props.okButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button key="ok" style={theme.buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||
{_('OK')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.cancelButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button key="cancel" style={Object.assign({}, theme.buttonStyle, { marginLeft: 10 })} onClick={cancelButton_click}>
|
||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>;
|
||||
}
|
||||
|
||||
module.exports = DialogButtonRow;
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
@@ -13,7 +15,7 @@ class DropboxLoginScreenComponent extends React.Component {
|
||||
this.shared_ = new Shared(this, msg => bridge().showInfoMessageBox(msg), msg => bridge().showErrorMessageBox(msg));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.shared_.refreshUrl();
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const Setting = require('lib/models/Setting');
|
||||
@@ -31,11 +33,11 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
return shared.refreshStats(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.initState(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.initState(nextProps);
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const styleSelector = require('./style/ExtensionBadge');
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -71,7 +73,7 @@ class HeaderComponent extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.windowCommand) {
|
||||
this.doCommand(nextProps.windowCommand);
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
@@ -8,7 +10,7 @@ const { filename, basename } = require('lib/path-utils.js');
|
||||
const { importEnex } = require('lib/import-enex');
|
||||
|
||||
class ImportScreenComponent extends React.Component {
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
doImport: true,
|
||||
filePath: this.props.filePath,
|
||||
@@ -16,7 +18,7 @@ class ImportScreenComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.filePath) {
|
||||
this.setState(
|
||||
{
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
|
||||
class ItemList extends React.Component {
|
||||
@@ -23,7 +25,7 @@ class ItemList extends React.Component {
|
||||
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
|
||||
const visibleItemCount = this.visibleItemCount(props);
|
||||
|
||||
let bottomItemIndex = topItemIndex + visibleItemCount;
|
||||
let bottomItemIndex = topItemIndex + (visibleItemCount - 1);
|
||||
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
|
||||
|
||||
this.setState({
|
||||
@@ -32,11 +34,11 @@ class ItemList extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
@@ -50,7 +52,7 @@ class ItemList extends React.Component {
|
||||
}
|
||||
|
||||
makeItemIndexVisible(itemIndex) {
|
||||
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex + 1);
|
||||
const top = Math.min(this.props.items.length - 1, this.state.topItemIndex);
|
||||
const bottom = Math.max(0, this.state.bottomItemIndex);
|
||||
|
||||
if (itemIndex >= top && itemIndex <= bottom) return;
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { Header } = require('./Header.min.js');
|
||||
@@ -6,6 +8,7 @@ const { NoteList } = require('./NoteList.min.js');
|
||||
const { NoteText } = require('./NoteText.min.js');
|
||||
const { PromptDialog } = require('./PromptDialog.min.js');
|
||||
const NotePropertiesDialog = require('./NotePropertiesDialog.min.js');
|
||||
const ShareNoteDialog = require('./ShareNoteDialog.js').default;
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
@@ -24,6 +27,7 @@ class MainScreenComponent extends React.Component {
|
||||
super();
|
||||
|
||||
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
|
||||
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
|
||||
this.sidebar_onDrag = this.sidebar_onDrag.bind(this);
|
||||
this.noteList_onDrag = this.noteList_onDrag.bind(this);
|
||||
}
|
||||
@@ -40,7 +44,11 @@ class MainScreenComponent extends React.Component {
|
||||
this.setState({ notePropertiesDialogOptions: {} });
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
shareNoteDialog_close() {
|
||||
this.setState({ shareNoteDialogOptions: {} });
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
promptOptions: null,
|
||||
modalLayer: {
|
||||
@@ -48,10 +56,11 @@ class MainScreenComponent extends React.Component {
|
||||
message: '',
|
||||
},
|
||||
notePropertiesDialogOptions: {},
|
||||
shareNoteDialogOptions: {},
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.windowCommand) {
|
||||
this.doCommand(newProps.windowCommand);
|
||||
}
|
||||
@@ -135,33 +144,53 @@ class MainScreenComponent extends React.Component {
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'setTags') {
|
||||
const tags = await Tag.tagsByNoteId(command.noteId);
|
||||
const noteTags = tags
|
||||
const tags = await Tag.commonTagsByNoteIds(command.noteIds);
|
||||
const startTags = tags
|
||||
.map(a => {
|
||||
return { value: a.id, label: a.title };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// sensitivity accent will treat accented characters as differemt
|
||||
// but treats caps as equal
|
||||
return a.label.localeCompare(b.label, undefined, {sensitivity: 'accent'});
|
||||
return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
|
||||
});
|
||||
const allTags = await Tag.allWithNotes();
|
||||
const tagSuggestions = allTags.map(a => {
|
||||
return { value: a.id, label: a.title };
|
||||
});
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// sensitivity accent will treat accented characters as differemt
|
||||
// but treats caps as equal
|
||||
return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
|
||||
});
|
||||
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
label: _('Add or remove tags:'),
|
||||
inputType: 'tags',
|
||||
value: noteTags,
|
||||
value: startTags,
|
||||
autocomplete: tagSuggestions,
|
||||
onClose: async answer => {
|
||||
if (answer !== null) {
|
||||
const tagTitles = answer.map(a => {
|
||||
const endTagTitles = answer.map(a => {
|
||||
return a.label.trim();
|
||||
});
|
||||
await Tag.setNoteTagsByTitles(command.noteId, tagTitles);
|
||||
if (command.noteIds.length === 1) {
|
||||
await Tag.setNoteTagsByTitles(command.noteIds[0], endTagTitles);
|
||||
} else {
|
||||
const startTagTitles = startTags.map(a => { return a.label.trim(); });
|
||||
const addTags = endTagTitles.filter(value => !startTagTitles.includes(value));
|
||||
const delTags = startTagTitles.filter(value => !endTagTitles.includes(value));
|
||||
|
||||
// apply the tag additions and deletions to each selected note
|
||||
for (let i = 0; i < command.noteIds.length; i++) {
|
||||
const tags = await Tag.tagsByNoteId(command.noteIds[i]);
|
||||
let tagTitles = tags.map(a => { return a.title; });
|
||||
tagTitles = tagTitles.concat(addTags);
|
||||
tagTitles = tagTitles.filter(value => !delTags.includes(value));
|
||||
await Tag.setNoteTagsByTitles(command.noteIds[i], tagTitles);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({ promptOptions: null });
|
||||
},
|
||||
@@ -247,6 +276,13 @@ class MainScreenComponent extends React.Component {
|
||||
onRevisionLinkClick: command.onRevisionLinkClick,
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'commandShareNoteDialog') {
|
||||
this.setState({
|
||||
shareNoteDialogOptions: {
|
||||
noteIds: command.noteIds,
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'toggleVisiblePanes') {
|
||||
this.toggleVisiblePanes();
|
||||
} else if (command.name === 'toggleSidebar') {
|
||||
@@ -362,14 +398,16 @@ class MainScreenComponent extends React.Component {
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
};
|
||||
|
||||
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
|
||||
|
||||
this.styles_.verticalResizer = {
|
||||
width: 5,
|
||||
height: height,
|
||||
// HACK: For unknown reasons, the resizers are just a little bit taller than the other elements,
|
||||
// making the whole window scroll vertically. So we remove 10 extra pixels here.
|
||||
height: rowHeight - 10,
|
||||
display: 'inline-block',
|
||||
};
|
||||
|
||||
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
|
||||
|
||||
this.styles_.sideBar = {
|
||||
width: sidebarWidth - this.styles_.verticalResizer.width,
|
||||
height: rowHeight,
|
||||
@@ -573,6 +611,7 @@ class MainScreenComponent extends React.Component {
|
||||
const modalLayerStyle = Object.assign({}, styles.modalLayer, { display: this.state.modalLayer.visible ? 'block' : 'none' });
|
||||
|
||||
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
const keyboardMode = Setting.value('editor.keyboardMode');
|
||||
|
||||
return (
|
||||
@@ -580,6 +619,7 @@ class MainScreenComponent extends React.Component {
|
||||
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
|
||||
|
||||
{notePropertiesDialogOptions.visible && <NotePropertiesDialog theme={this.props.theme} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
|
||||
{shareNoteDialogOptions.visible && <ShareNoteDialog theme={this.props.theme} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
|
||||
|
||||
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} theme={this.props.theme} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
|
||||
|
||||
@@ -589,7 +629,7 @@ class MainScreenComponent extends React.Component {
|
||||
<VerticalResizer style={styles.verticalResizer} onDrag={this.sidebar_onDrag} />
|
||||
<NoteList style={styles.noteList} />
|
||||
<VerticalResizer style={styles.verticalResizer} onDrag={this.noteList_onDrag} />
|
||||
<NoteText style={styles.noteText} keyboardMode={keyboardMode} visiblePanes={this.props.noteVisiblePanes} noteDevToolsVisible={this.props.noteDevToolsVisible} />
|
||||
<NoteText style={styles.noteText} keyboardMode={keyboardMode} visiblePanes={this.props.noteVisiblePanes} />
|
||||
|
||||
{pluginDialog}
|
||||
</div>
|
||||
@@ -613,7 +653,6 @@ const mapStateToProps = state => {
|
||||
noteListWidth: state.settings['style.noteList.width'],
|
||||
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||
plugins: state.plugins,
|
||||
noteDevToolsVisible: state.noteDevToolsVisible,
|
||||
templates: state.templates,
|
||||
};
|
||||
};
|
||||
|
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
class NavigatorComponent extends Component {
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.route) {
|
||||
const screenInfo = this.props.screens[newProps.route.routeName];
|
||||
let windowTitle = ['Joplin'];
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const { ItemList } = require('./ItemList.min.js');
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
@@ -87,6 +89,7 @@ class NoteListComponent extends React.Component {
|
||||
const menu = NoteListUtils.makeContextMenu(noteIds, {
|
||||
notes: this.props.notes,
|
||||
dispatch: this.props.dispatch,
|
||||
watchedNoteFiles: this.props.watchedNoteFiles,
|
||||
});
|
||||
|
||||
menu.popup(bridge().window());
|
||||
@@ -292,20 +295,49 @@ class NoteListComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
scrollNoteIndex_(keyCode, ctrlKey, metaKey, noteIndex) {
|
||||
|
||||
if (keyCode === 33) {
|
||||
// Page Up
|
||||
noteIndex -= (this.itemListRef.current.visibleItemCount() - 1);
|
||||
|
||||
} else if (keyCode === 34) {
|
||||
// Page Down
|
||||
noteIndex += (this.itemListRef.current.visibleItemCount() - 1);
|
||||
|
||||
} else if ((keyCode === 35 && ctrlKey) || (keyCode === 40 && metaKey)) {
|
||||
// CTRL+End, CMD+Down
|
||||
noteIndex = this.props.notes.length - 1;
|
||||
|
||||
} else if ((keyCode === 36 && ctrlKey) || (keyCode === 38 && metaKey)) {
|
||||
// CTRL+Home, CMD+Up
|
||||
noteIndex = 0;
|
||||
|
||||
} else if (keyCode === 38 && !metaKey) {
|
||||
// Up
|
||||
noteIndex -= 1;
|
||||
|
||||
} else if (keyCode === 40 && !metaKey) {
|
||||
// Down
|
||||
noteIndex += 1;
|
||||
}
|
||||
|
||||
if (noteIndex < 0) noteIndex = 0;
|
||||
if (noteIndex > this.props.notes.length - 1) noteIndex = this.props.notes.length - 1;
|
||||
|
||||
return noteIndex;
|
||||
}
|
||||
|
||||
async onKeyDown(event) {
|
||||
const keyCode = event.keyCode;
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
|
||||
if (noteIds.length === 1 && (keyCode === 40 || keyCode === 38)) {
|
||||
// DOWN / UP
|
||||
if (noteIds.length === 1 && (keyCode === 40 || keyCode === 38 || keyCode === 33 || keyCode === 34 || keyCode === 35 || keyCode == 36)) {
|
||||
// DOWN / UP / PAGEDOWN / PAGEUP / END / HOME
|
||||
const noteId = noteIds[0];
|
||||
let noteIndex = BaseModel.modelIndexById(this.props.notes, noteId);
|
||||
const inc = keyCode === 38 ? -1 : +1;
|
||||
|
||||
noteIndex += inc;
|
||||
|
||||
if (noteIndex < 0) noteIndex = 0;
|
||||
if (noteIndex > this.props.notes.length - 1) noteIndex = this.props.notes.length - 1;
|
||||
noteIndex = this.scrollNoteIndex_(keyCode, event.ctrlKey, event.metaKey, noteIndex);
|
||||
|
||||
const newSelectedNote = this.props.notes[noteIndex];
|
||||
|
||||
@@ -361,6 +393,15 @@ class NoteListComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.keyCode === 65 && (event.ctrlKey || event.metaKey)) {
|
||||
// Ctrl+A key
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECT_ALL',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
focusNoteId_(noteId) {
|
||||
|
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const DialogButtonRow = require('./DialogButtonRow.min');
|
||||
const Datetime = require('react-datetime');
|
||||
const Note = require('lib/models/Note');
|
||||
const formatcoords = require('formatcoords');
|
||||
@@ -11,10 +14,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.okButton_click = this.okButton_click.bind(this);
|
||||
this.cancelButton_click = this.cancelButton_click.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.revisionsLink_click = this.revisionsLink_click.bind(this);
|
||||
this.buttonRow_click = this.buttonRow_click.bind(this);
|
||||
this.okButton = React.createRef();
|
||||
|
||||
this.state = {
|
||||
@@ -27,6 +28,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
id: _('ID'),
|
||||
user_created_time: _('Created'),
|
||||
user_updated_time: _('Updated'),
|
||||
todo_completed: _('Completed'),
|
||||
location: _('Location'),
|
||||
source_url: _('URL'),
|
||||
revisionsLink: _('Note History'),
|
||||
@@ -72,6 +74,11 @@ class NotePropertiesDialog extends React.Component {
|
||||
|
||||
formNote.user_updated_time = time.formatMsToLocal(note.user_updated_time);
|
||||
formNote.user_created_time = time.formatMsToLocal(note.user_created_time);
|
||||
|
||||
if (note.todo_completed) {
|
||||
formNote.todo_completed = time.formatMsToLocal(note.todo_completed);
|
||||
}
|
||||
|
||||
formNote.source_url = note.source_url;
|
||||
|
||||
formNote.location = '';
|
||||
@@ -90,6 +97,11 @@ class NotePropertiesDialog extends React.Component {
|
||||
const note = Object.assign({ id: formNote.id }, this.latLongFromLocation(formNote.location));
|
||||
note.user_created_time = time.formatLocalToMs(formNote.user_created_time);
|
||||
note.user_updated_time = time.formatLocalToMs(formNote.user_updated_time);
|
||||
|
||||
if (formNote.todo_completed) {
|
||||
note.todo_completed = time.formatMsToLocal(formNote.todo_completed);
|
||||
}
|
||||
|
||||
note.source_url = formNote.source_url;
|
||||
|
||||
return note;
|
||||
@@ -107,6 +119,9 @@ class NotePropertiesDialog extends React.Component {
|
||||
this.styles_.controlBox = {
|
||||
marginBottom: '1em',
|
||||
color: 'black', // This will apply for the calendar
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
this.styles_.button = {
|
||||
@@ -123,8 +138,11 @@ class NotePropertiesDialog extends React.Component {
|
||||
color: theme.color,
|
||||
textDecoration: 'none',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
padding: '.14em',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '0.5em',
|
||||
};
|
||||
|
||||
this.styles_.input = {
|
||||
@@ -153,12 +171,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
okButton_click() {
|
||||
this.closeDialog(true);
|
||||
}
|
||||
|
||||
cancelButton_click() {
|
||||
this.closeDialog(false);
|
||||
buttonRow_click(event) {
|
||||
this.closeDialog(event.buttonName === 'ok');
|
||||
}
|
||||
|
||||
revisionsLink_click() {
|
||||
@@ -166,14 +180,6 @@ class NotePropertiesDialog extends React.Component {
|
||||
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.keyCode === 13) {
|
||||
this.closeDialog(true);
|
||||
} else if (event.keyCode === 27) {
|
||||
this.closeDialog(false);
|
||||
}
|
||||
}
|
||||
|
||||
editPropertyButtonClick(key, initialValue) {
|
||||
this.setState({
|
||||
editedKey: key,
|
||||
@@ -218,15 +224,12 @@ class NotePropertiesDialog extends React.Component {
|
||||
async cancelProperty() {
|
||||
return new Promise((resolve) => {
|
||||
this.okButton.current.focus();
|
||||
this.setState(
|
||||
{
|
||||
editedKey: null,
|
||||
editedValue: null,
|
||||
},
|
||||
() => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
this.setState({
|
||||
editedKey: null,
|
||||
editedValue: null,
|
||||
}, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -328,7 +331,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
if (editCompHandler) {
|
||||
editComp = (
|
||||
<a href="#" onClick={editCompHandler} style={styles.editPropertyButton}>
|
||||
<i className={`fa ${editCompIcon}`} aria-hidden="true" style={{ marginLeft: '.5em' }}></i>
|
||||
<i className={`fa ${editCompIcon}`} aria-hidden="true"></i>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -354,7 +357,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
return dms.format('DDMMss', { decimalPlaces: 0 });
|
||||
}
|
||||
|
||||
if (['user_updated_time', 'user_created_time'].indexOf(key) >= 0) {
|
||||
if (['user_updated_time', 'user_created_time', 'todo_completed'].indexOf(key) >= 0) {
|
||||
return time.formatMsToLocal(note[key]);
|
||||
}
|
||||
|
||||
@@ -363,21 +366,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const styles = this.styles(this.props.theme);
|
||||
const formNote = this.state.formNote;
|
||||
|
||||
const buttonComps = [];
|
||||
buttonComps.push(
|
||||
<button key="ok" style={styles.button} onClick={this.okButton_click} ref={this.okButton} onKeyDown={this.onKeyDown}>
|
||||
{_('Apply')}
|
||||
</button>
|
||||
);
|
||||
buttonComps.push(
|
||||
<button key="cancel" style={styles.button} onClick={this.cancelButton_click}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const noteComps = [];
|
||||
|
||||
if (formNote) {
|
||||
@@ -393,7 +383,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
<div style={theme.dialogBox}>
|
||||
<div style={theme.dialogTitle}>{_('Note properties')}</div>
|
||||
<div>{noteComps}</div>
|
||||
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
|
||||
<DialogButtonRow theme={this.props.theme} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -6,16 +8,16 @@ const NoteTextViewer = require('./NoteTextViewer.min');
|
||||
const HelpButton = require('./HelpButton.min');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Revision = require('lib/models/Revision');
|
||||
const Note = require('lib/models/Note');
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const MarkupToHtml = require('lib/renderers/MarkupToHtml');
|
||||
const { MarkupToHtml, assetsToHeaders } = require('lib/joplin-renderer');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const ReactTooltip = require('react-tooltip');
|
||||
const { urlDecode, substrWithEllipsis } = require('lib/string-utils');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const markupLanguageUtils = require('lib/markupLanguageUtils');
|
||||
|
||||
class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
constructor() {
|
||||
@@ -101,7 +103,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
|
||||
async reloadNote() {
|
||||
let noteBody = '';
|
||||
let markupLanguage = Note.MARKUP_LANGUAGE_MARKDOWN;
|
||||
let markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
if (!this.state.revisions.length || !this.state.currentRevId) {
|
||||
noteBody = _('This note has no history');
|
||||
this.setState({ note: null });
|
||||
@@ -116,18 +118,22 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const markupToHtml = new MarkupToHtml({
|
||||
const markupToHtml = markupLanguageUtils.newMarkupToHtml({
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
});
|
||||
|
||||
const result = markupToHtml.render(markupLanguage, noteBody, theme, {
|
||||
const result = await markupToHtml.render(markupLanguage, noteBody, theme, {
|
||||
codeTheme: theme.codeThemeCss,
|
||||
userCss: this.props.customCss ? this.props.customCss : '',
|
||||
resources: await shared.attachedResources(noteBody),
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
});
|
||||
|
||||
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, { cssFiles: result.cssFiles });
|
||||
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, {
|
||||
cssFiles: result.cssFiles,
|
||||
pluginAssets: result.pluginAssets,
|
||||
pluginAssetsHeadersHtml: assetsToHeaders(result.pluginAssets),
|
||||
});
|
||||
}
|
||||
|
||||
async webview_ipcMessage(event) {
|
||||
@@ -196,7 +202,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
</div>
|
||||
);
|
||||
|
||||
const viewer = <NoteTextViewer viewerStyle={{ display: 'flex', flex: 1 }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
|
||||
const viewer = <NoteTextViewer viewerStyle={{ display: 'flex', flex: 1, borderLeft: 'none' }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
|
||||
|
||||
return (
|
||||
<div style={style.root}>
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -7,15 +9,13 @@ class NoteSearchBarComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
};
|
||||
|
||||
this.searchInput_change = this.searchInput_change.bind(this);
|
||||
this.searchInput_keyDown = this.searchInput_keyDown.bind(this);
|
||||
this.previousButton_click = this.previousButton_click.bind(this);
|
||||
this.nextButton_click = this.nextButton_click.bind(this);
|
||||
this.closeButton_click = this.closeButton_click.bind(this);
|
||||
|
||||
this.backgroundColor = undefined;
|
||||
}
|
||||
|
||||
style() {
|
||||
@@ -35,7 +35,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
this.refs.searchInput.focus();
|
||||
}
|
||||
|
||||
buttonIconComponent(iconName, clickHandler) {
|
||||
buttonIconComponent(iconName, clickHandler, isEnabled) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const searchButton = {
|
||||
@@ -51,6 +51,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
display: 'flex',
|
||||
fontSize: Math.round(theme.fontSize) * 1.2,
|
||||
color: theme.color,
|
||||
opacity: isEnabled ? 1.0 : theme.disabledOpacity,
|
||||
};
|
||||
|
||||
const icon = <i style={iconStyle} className={`fa ${iconName}`}></i>;
|
||||
@@ -64,7 +65,6 @@ class NoteSearchBarComponent extends React.Component {
|
||||
|
||||
searchInput_change(event) {
|
||||
const query = event.currentTarget.value;
|
||||
this.setState({ query: query });
|
||||
this.triggerOnChange(query);
|
||||
}
|
||||
|
||||
@@ -86,6 +86,13 @@ class NoteSearchBarComponent extends React.Component {
|
||||
|
||||
if (this.props.onClose) this.props.onClose();
|
||||
}
|
||||
|
||||
if (event.keyCode === 70) {
|
||||
// F key
|
||||
if (event.ctrlKey) {
|
||||
event.target.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousButton_click() {
|
||||
@@ -109,17 +116,56 @@ class NoteSearchBarComponent extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const closeButton = this.buttonIconComponent('fa-times', this.closeButton_click);
|
||||
const previousButton = this.buttonIconComponent('fa-chevron-up', this.previousButton_click);
|
||||
const nextButton = this.buttonIconComponent('fa-chevron-down', this.nextButton_click);
|
||||
const query = this.props.query ? this.props.query : '';
|
||||
|
||||
// backgroundColor needs to cached to a local variable to prevent the
|
||||
// colour from blinking.
|
||||
// For more info: https://github.com/laurent22/joplin/pull/2329#issuecomment-578376835
|
||||
const theme = themeStyle(this.props.theme);
|
||||
if (!this.props.searching) {
|
||||
if (this.props.resultCount === 0 && query.length > 0) {
|
||||
this.backgroundColor = theme.warningBackgroundColor;
|
||||
} else {
|
||||
this.backgroundColor = theme.backgroundColor;
|
||||
}
|
||||
}
|
||||
if (this.backgroundColor === undefined) {
|
||||
this.backgroundColor = theme.backgroundColor;
|
||||
}
|
||||
let buttonEnabled = (this.backgroundColor === theme.backgroundColor);
|
||||
|
||||
const closeButton = this.buttonIconComponent('fa-times', this.closeButton_click, true);
|
||||
const previousButton = this.buttonIconComponent('fa-chevron-up', this.previousButton_click, buttonEnabled);
|
||||
const nextButton = this.buttonIconComponent('fa-chevron-down', this.nextButton_click, buttonEnabled);
|
||||
|
||||
const textStyle = Object.assign({
|
||||
fontSize: theme.fontSize,
|
||||
fontFamily: theme.fontFamily,
|
||||
color: theme.colorFaded,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
});
|
||||
const matchesFoundString = (query.length > 0 && this.props.resultCount > 0) ? (
|
||||
<div style={textStyle}>
|
||||
{`${this.props.selectedIndex + 1} / ${this.props.resultCount}`}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{closeButton}
|
||||
<input placeholder={_('Search...')} value={this.state.query} onChange={this.searchInput_change} onKeyDown={this.searchInput_keyDown} ref="searchInput" type="text" style={{ width: 200, marginRight: 5 }}></input>
|
||||
<input
|
||||
placeholder={_('Search...')}
|
||||
value={query}
|
||||
onChange={this.searchInput_change}
|
||||
onKeyDown={this.searchInput_keyDown}
|
||||
ref="searchInput"
|
||||
type="text"
|
||||
style={{ width: 200, marginRight: 5, backgroundColor: this.backgroundColor }}
|
||||
/>
|
||||
{nextButton}
|
||||
{previousButton}
|
||||
{matchesFoundString}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|