You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
141 Commits
android-v1
...
v1.0.183
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
@@ -47,8 +47,9 @@ Server/bin/
|
||||
Server/node_modules/
|
||||
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
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
14
.eslintrc.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",
|
||||
],
|
||||
};
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -49,4 +49,4 @@ Tools/commit_hook.txt
|
||||
# Ignore files generated from TypeScript files
|
||||
ElectronClient/app/gui/ShareNoteDialog.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/pluginAssetsLoader.js
|
||||
ReactNativeClient/PluginAssetsLoader.js
|
||||
|
BIN
Assets/GitHubSponsorIcon.png
Normal file
BIN
Assets/GitHubSponsorIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
8
BUILD.md
8
BUILD.md
@@ -30,11 +30,13 @@ All TypeScript files are generated next to the .ts or .tsx file. So for example,
|
||||
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
|
||||
@@ -53,7 +55,9 @@ 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:
|
||||
|
||||
```
|
||||
xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
|
||||
|
@@ -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];
|
||||
|
@@ -14,10 +14,12 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
options() {
|
||||
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.')]];
|
||||
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) => {
|
||||
@@ -103,12 +105,8 @@ class Command extends BaseCommand {
|
||||
this.stdout(renderKeyValue(args.name));
|
||||
}
|
||||
|
||||
app()
|
||||
.gui()
|
||||
.showConsole();
|
||||
app()
|
||||
.gui()
|
||||
.maximizeConsole();
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
|
||||
return;
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ class Command extends BaseCommand {
|
||||
info: stdoutFn,
|
||||
warn: stdoutFn,
|
||||
error: stdoutFn,
|
||||
}});
|
||||
} });
|
||||
ClipperServer.instance().setDispatch(() => {});
|
||||
ClipperServer.instance().setLogger(clipperLogger);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
374
CliClient/package-lock.json
generated
374
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.154",
|
||||
"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",
|
||||
@@ -964,24 +969,6 @@
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
|
||||
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
|
||||
"requires": {
|
||||
"neo-async": "^2.6.0",
|
||||
"optimist": "^0.6.1",
|
||||
"source-map": "^0.6.1",
|
||||
"uglify-js": "^3.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
@@ -1050,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",
|
||||
@@ -1154,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"
|
||||
}
|
||||
@@ -1556,75 +1543,6 @@
|
||||
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
|
||||
"dev": true
|
||||
},
|
||||
"joplin-renderer": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/joplin-renderer/-/joplin-renderer-1.0.5.tgz",
|
||||
"integrity": "sha512-6WHqJdHzpxkI4AIz7/uK+V4eB+uU3kg8ut8r1HwC10kWQCMdTtH0yv04l+cNpWvQFULsZANmVr7FAP6JNhcqcw==",
|
||||
"requires": {
|
||||
"base-64": "^0.1.0",
|
||||
"font-awesome-filetypes": "^2.1.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"highlight.js": "^9.17.1",
|
||||
"html-entities": "^1.2.1",
|
||||
"katex": "^0.11.1",
|
||||
"markdown-it": "^10.0.0",
|
||||
"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-footnote": "^3.0.2",
|
||||
"markdown-it-ins": "^3.0.0",
|
||||
"markdown-it-mark": "^3.0.0",
|
||||
"markdown-it-multimd-table": "^4.0.0",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-toc-done-right": "^4.1.0",
|
||||
"md5": "^2.2.1",
|
||||
"uslug": "^1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
|
||||
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.17.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.17.1.tgz",
|
||||
"integrity": "sha512-TA2/doAur5Ol8+iM3Ov7qy3jYcr/QiJ2eDTdRF4dfbjG7AaaB99J5G+zSl11ljbl6cIcahgPY6SKb3sC3EJ0fw==",
|
||||
"requires": {
|
||||
"handlebars": "^4.5.3"
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"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": "~2.0.0",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"joplin-turndown": {
|
||||
"version": "4.0.19",
|
||||
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.19.tgz",
|
||||
@@ -1636,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",
|
||||
@@ -1776,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"
|
||||
}
|
||||
@@ -1807,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",
|
||||
@@ -1837,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": {
|
||||
@@ -1851,15 +1781,22 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -1882,6 +1819,14 @@
|
||||
"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",
|
||||
@@ -1898,11 +1843,25 @@
|
||||
"integrity": "sha512-HqMWeKfMMOu4zBO0emmxsoMWmbf2cPKZY1wP6FsTbKmicFfp5y4L3KXAsNeO1rM6NTJVOrNlLKMPjWzriBGspw=="
|
||||
},
|
||||
"markdown-it-multimd-table": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-multimd-table/-/markdown-it-multimd-table-4.0.0.tgz",
|
||||
"integrity": "sha512-kdM3fH+/sRMfHQgD2CM1BcIpLNODUCuoiFr6TwS7mDJBYntVXDJxZbFwGDRflIc9ZzAfsUbr0lnHc6RbYafIsw==",
|
||||
"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": {
|
||||
@@ -2024,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",
|
||||
@@ -2052,35 +2011,30 @@
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"neo-async": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
|
||||
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
|
||||
},
|
||||
"nextgen-events": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/nextgen-events/-/nextgen-events-1.3.0.tgz",
|
||||
@@ -2166,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"
|
||||
@@ -2259,15 +2221,6 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"optimist": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
||||
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
|
||||
"requires": {
|
||||
"minimist": "~0.0.1",
|
||||
"wordwrap": "~0.0.2"
|
||||
}
|
||||
},
|
||||
"optionator": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
|
||||
@@ -2855,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": {
|
||||
@@ -3341,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",
|
||||
@@ -3525,11 +3354,6 @@
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
|
||||
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
|
||||
},
|
||||
"wordwrapjs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-3.0.0.tgz",
|
||||
|
@@ -20,7 +20,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.154",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -44,12 +44,11 @@
|
||||
"html-minifier": "^3.5.15",
|
||||
"image-data-uri": "^2.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"joplin-renderer": "^1.0.5",
|
||||
"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",
|
||||
@@ -67,7 +66,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",
|
||||
@@ -81,7 +80,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
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
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
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
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
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
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);
|
||||
}));
|
||||
|
||||
});
|
336
CliClient/tests/services_InteropService_Exporter_Md.js
Normal file
336
CliClient/tests/services_InteropService_Exporter_Md.js
Normal file
@@ -0,0 +1,336 @@
|
||||
/* 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 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.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);
|
||||
|
||||
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
BIN
CliClient/tests/support/photo-two.jpg
Normal file
Binary file not shown.
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);
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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>;
|
||||
|
@@ -124,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() {
|
||||
@@ -187,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', () => {
|
||||
|
@@ -5,17 +5,19 @@ const Setting = require('lib/models/Setting');
|
||||
const md5 = require('md5');
|
||||
const url = require('url');
|
||||
const { shim } = require('lib/shim');
|
||||
// const { BrowserWindow } = require('electron');
|
||||
|
||||
class InteropServiceHelper {
|
||||
|
||||
static async exportNoteToHtmlFile(noteId) {
|
||||
static async exportNoteToHtmlFile(noteId, exportOptions) {
|
||||
const tempFile = `${Setting.value('tempDir')}/${md5(Date.now() + Math.random())}.html`;
|
||||
const exportOptions = {};
|
||||
exportOptions.path = tempFile;
|
||||
exportOptions.format = 'html';
|
||||
exportOptions.target = 'file';
|
||||
exportOptions.sourceNoteIds = [noteId];
|
||||
|
||||
exportOptions = Object.assign({}, {
|
||||
path: tempFile,
|
||||
format: 'html',
|
||||
target: 'file',
|
||||
sourceNoteIds: [noteId],
|
||||
customCss: '',
|
||||
}, exportOptions);
|
||||
|
||||
const service = new InteropService();
|
||||
|
||||
@@ -34,10 +36,14 @@ class InteropServiceHelper {
|
||||
};
|
||||
|
||||
try {
|
||||
htmlFile = await this.exportNoteToHtmlFile(noteId);
|
||||
const exportOptions = {
|
||||
customCss: options.customCss ? options.customCss : '',
|
||||
};
|
||||
|
||||
htmlFile = await this.exportNoteToHtmlFile(noteId, exportOptions);
|
||||
|
||||
const windowOptions = {
|
||||
show: true,
|
||||
show: false,
|
||||
};
|
||||
|
||||
win = bridge().newBrowserWindow(windowOptions);
|
||||
@@ -55,12 +61,12 @@ class InteropServiceHelper {
|
||||
cleanup();
|
||||
}
|
||||
} else {
|
||||
win.webContents.print(options, (success) => {
|
||||
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) reject(new Error('Could not print'));
|
||||
if (!success && reason !== 'cancelled') reject(new Error(`Could not print: ${reason}`));
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
@@ -93,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({
|
||||
|
@@ -6,7 +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('joplin-renderer');
|
||||
const { MarkupToHtml } = require('lib/joplin-renderer');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const fs = require('fs-extra');
|
||||
@@ -408,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({
|
||||
@@ -462,6 +462,7 @@ class Application extends BaseApplication {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'exportPdf',
|
||||
noteId: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -628,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}`);
|
||||
@@ -872,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,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
@@ -1130,7 +1130,7 @@ class Application extends BaseApplication {
|
||||
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 === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
|
||||
@@ -1149,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(); } },
|
||||
]);
|
||||
@@ -1189,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');
|
||||
|
||||
|
@@ -56,7 +56,7 @@ class Bridge {
|
||||
}
|
||||
|
||||
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.showSaveDialogSync(this.window(), options);
|
||||
@@ -67,7 +67,7 @@ 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;
|
||||
@@ -80,7 +80,7 @@ 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.showMessageBoxSync(window, options);
|
||||
}
|
||||
@@ -89,6 +89,7 @@ class Bridge {
|
||||
return this.showMessageBox_(this.window(), {
|
||||
type: 'error',
|
||||
message: message,
|
||||
buttons: [_('OK')],
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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 };
|
||||
|
@@ -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');
|
||||
@@ -210,7 +212,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
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>;
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -573,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';
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
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 { bridge } = require('electron').remote.require('./bridge');
|
||||
|
@@ -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');
|
||||
|
@@ -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');
|
||||
|
@@ -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');
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
|
||||
class ItemList extends React.Component {
|
||||
|
@@ -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');
|
||||
@@ -142,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 });
|
||||
},
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
|
@@ -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,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -26,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'),
|
||||
@@ -71,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 = '';
|
||||
@@ -89,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;
|
||||
@@ -106,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 = {
|
||||
@@ -122,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 = {
|
||||
@@ -312,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>
|
||||
);
|
||||
}
|
||||
@@ -338,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]);
|
||||
}
|
||||
|
||||
|
@@ -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');
|
||||
@@ -10,7 +12,7 @@ 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('joplin-renderer');
|
||||
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');
|
||||
@@ -127,7 +129,11 @@ class NoteRevisionViewerComponent extends React.PureComponent {
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
});
|
||||
|
||||
this.viewerRef_.current.wrappedInstance.send('setHtml', result.html, { cssFiles: result.cssFiles, pluginAssets: result.pluginAssets });
|
||||
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>
|
||||
);
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
@@ -1,10 +1,11 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const InteropServiceHelper = require('../InteropServiceHelper.js');
|
||||
@@ -15,7 +16,7 @@ const TagList = require('./TagList.min.js');
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { MarkupToHtml } = require('joplin-renderer');
|
||||
const { MarkupToHtml, assetsToHeaders } = require('lib/joplin-renderer');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -81,7 +82,7 @@ class CustomMdMode extends ace.acequire('ace/mode/markdown').Mode {
|
||||
}
|
||||
}
|
||||
|
||||
const NOTE_TAG_BAR_FEATURE_ENABLED = false;
|
||||
const NOTE_TAG_BAR_FEATURE_ENABLED = true;
|
||||
|
||||
class NoteTextComponent extends React.Component {
|
||||
constructor() {
|
||||
@@ -91,6 +92,7 @@ class NoteTextComponent extends React.Component {
|
||||
query: '',
|
||||
selectedIndex: 0,
|
||||
resultCount: 0,
|
||||
searching: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
@@ -313,6 +315,8 @@ class NoteTextComponent extends React.Component {
|
||||
query: query,
|
||||
selectedIndex: 0,
|
||||
timestamp: Date.now(),
|
||||
resultCount: this.state.localSearch.resultCount,
|
||||
searching: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -539,7 +543,6 @@ class NoteTextComponent extends React.Component {
|
||||
let note = null;
|
||||
let loadingNewNote = true;
|
||||
let parentFolder = null;
|
||||
let noteTags = [];
|
||||
let scrollPercent = 0;
|
||||
|
||||
if (props.newNote) {
|
||||
@@ -554,7 +557,6 @@ class NoteTextComponent extends React.Component {
|
||||
if (!scrollPercent) scrollPercent = 0;
|
||||
|
||||
loadingNewNote = stateNoteId !== noteId;
|
||||
noteTags = await Tag.tagsByNoteId(noteId);
|
||||
this.lastLoadedNoteId_ = noteId;
|
||||
note = noteId ? await Note.load(noteId) : null;
|
||||
if (noteId !== this.lastLoadedNoteId_) return defer(); // Race condition - current note was changed while this one was loading
|
||||
@@ -633,7 +635,6 @@ class NoteTextComponent extends React.Component {
|
||||
webviewReady: webviewReady,
|
||||
folder: parentFolder,
|
||||
lastKeys: [],
|
||||
noteTags: noteTags,
|
||||
showRevisions: false,
|
||||
};
|
||||
|
||||
@@ -654,22 +655,6 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
// https://github.com/laurent22/joplin/pull/893#discussion_r228025210
|
||||
// @Abijeet: Had to add this check. If not, was going into an infinite loop where state was getting updated repeatedly.
|
||||
// Since I'm updating the state, the componentWillReceiveProps was getting triggered again, where nextProps.newNote was still true, causing reloadNote to trigger again and again.
|
||||
// Notes from Laurent: The selected note tags are part of the global Redux state because they need to be updated whenever tags are changed or deleted
|
||||
// anywhere in the app. Thus it's not possible simple to load the tags here (as we won't have a way to know if they're updated afterwards).
|
||||
// Perhaps a better way would be to move that code in the middleware, check for TAGS_DELETE, TAGS_UPDATE, etc. actions and update the
|
||||
// selected note tags accordingly.
|
||||
if (NOTE_TAG_BAR_FEATURE_ENABLED) {
|
||||
if (!this.props.newNote) {
|
||||
this.props.dispatch({
|
||||
type: 'SET_NOTE_TAGS',
|
||||
items: noteTags,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if (newState.note) await shared.refreshAttachedResources(this, newState.note.body);
|
||||
|
||||
await this.updateHtml(newState.note ? newState.note.markup_language : null, newState.note ? newState.note.body : '');
|
||||
@@ -709,18 +694,38 @@ class NoteTextComponent extends React.Component {
|
||||
if (newTags.length !== oldTags.length) return true;
|
||||
|
||||
for (let i = 0; i < newTags.length; ++i) {
|
||||
let found = false;
|
||||
let currNewTag = newTags[i];
|
||||
for (let j = 0; j < oldTags.length; ++j) {
|
||||
let currOldTag = oldTags[j];
|
||||
if (currOldTag.id === currNewTag.id && currOldTag.updated_time !== currNewTag.updated_time) {
|
||||
return true;
|
||||
if (currOldTag.id === currNewTag.id) {
|
||||
found = true;
|
||||
if (currOldTag.updated_time !== currNewTag.updated_time) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
canDisplayTagBar() {
|
||||
if (!NOTE_TAG_BAR_FEATURE_ENABLED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.state.noteTags || this.state.noteTags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async noteRevisionViewer_onBack() {
|
||||
// When coming back from the revision viewer, the webview has been
|
||||
// unmounted so will need to reload. We set webviewReady to false
|
||||
@@ -765,6 +770,7 @@ class NoteTextComponent extends React.Component {
|
||||
} else if (msg === 'setMarkerCount') {
|
||||
const ls = Object.assign({}, this.state.localSearch);
|
||||
ls.resultCount = arg0;
|
||||
ls.searching = false;
|
||||
this.setState({ localSearch: ls });
|
||||
} else if (msg.indexOf('markForDownload:') === 0) {
|
||||
const s = msg.split(':');
|
||||
@@ -1111,9 +1117,11 @@ class NoteTextComponent extends React.Component {
|
||||
if (!command) return;
|
||||
|
||||
let fn = null;
|
||||
let args = null;
|
||||
|
||||
if (command.name === 'exportPdf') {
|
||||
fn = this.commandSavePdf;
|
||||
args = { noteId: command.noteId };
|
||||
} else if (command.name === 'print') {
|
||||
fn = this.commandPrint;
|
||||
}
|
||||
@@ -1129,6 +1137,8 @@ class NoteTextComponent extends React.Component {
|
||||
fn = this.commandDateTime;
|
||||
} else if (command.name === 'commandStartExternalEditing') {
|
||||
fn = this.commandStartExternalEditing;
|
||||
} else if (command.name === 'commandStopExternalEditing') {
|
||||
fn = this.commandStopExternalEditing;
|
||||
} else if (command.name === 'showLocalSearch') {
|
||||
fn = this.commandShowLocalSearch;
|
||||
} else if (command.name === 'textCode') {
|
||||
@@ -1163,7 +1173,7 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
fn = fn.bind(this);
|
||||
fn();
|
||||
fn(args);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1171,7 +1181,9 @@ class NoteTextComponent extends React.Component {
|
||||
if (this.state.showLocalSearch) {
|
||||
this.noteSearchBar_.current.wrappedInstance.focus();
|
||||
} else {
|
||||
this.setState({ showLocalSearch: true });
|
||||
this.setState({
|
||||
showLocalSearch: true,
|
||||
localSearch: Object.assign({}, this.localSearchDefaultState) });
|
||||
}
|
||||
|
||||
this.props.dispatch({
|
||||
@@ -1256,10 +1268,11 @@ class NoteTextComponent extends React.Component {
|
||||
setTimeout(async () => {
|
||||
if (target === 'pdf') {
|
||||
try {
|
||||
const pdfData = await InteropServiceHelper.exportNoteToPdf(this.state.note.id, {
|
||||
const pdfData = await InteropServiceHelper.exportNoteToPdf(options.noteId, {
|
||||
printBackground: true,
|
||||
pageSize: Setting.value('export.pdfPageSize'),
|
||||
landscape: Setting.value('export.pdfPageOrientation') === 'landscape',
|
||||
customCss: this.props.customCss,
|
||||
});
|
||||
await shim.fsDriver().writeFile(options.path, pdfData, 'buffer');
|
||||
} catch (error) {
|
||||
@@ -1268,8 +1281,9 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
} else if (target === 'printer') {
|
||||
try {
|
||||
await InteropServiceHelper.printNote(this.state.note.id, {
|
||||
await InteropServiceHelper.printNote(options.noteId, {
|
||||
printBackground: true,
|
||||
customCss: this.props.customCss,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -1282,18 +1296,20 @@ class NoteTextComponent extends React.Component {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async commandSavePdf() {
|
||||
async commandSavePdf(args) {
|
||||
try {
|
||||
if (!this.state.note) throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
|
||||
if (!this.state.note && !args.noteId) throw new Error(_('Only one note can be exported to PDF at a time.'));
|
||||
|
||||
const note = (!args.noteId ? this.state.note : await Note.load(args.noteId));
|
||||
|
||||
const path = bridge().showSaveDialog({
|
||||
filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
|
||||
defaultPath: safeFilename(this.state.note.title),
|
||||
defaultPath: safeFilename(note.title),
|
||||
});
|
||||
|
||||
if (!path) return;
|
||||
|
||||
await this.printTo_('pdf', { path: path });
|
||||
await this.printTo_('pdf', { path: path, noteId: note.id });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
@@ -1301,25 +1317,23 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
async commandPrint() {
|
||||
try {
|
||||
await this.printTo_('printer');
|
||||
if (!this.state.note) throw new Error(_('Only one note can be printed at a time.'));
|
||||
|
||||
await this.printTo_('printer', { noteId: this.state.note.id });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async commandStartExternalEditing() {
|
||||
try {
|
||||
await this.saveIfNeeded(true, {
|
||||
autoTitle: false,
|
||||
});
|
||||
await ExternalEditWatcher.instance().openAndWatch(this.state.note);
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
|
||||
}
|
||||
await this.saveIfNeeded(true, {
|
||||
autoTitle: false,
|
||||
});
|
||||
NoteListUtils.startExternalEditing(this.state.note.id);
|
||||
}
|
||||
|
||||
async commandStopExternalEditing() {
|
||||
ExternalEditWatcher.instance().stopWatching(this.state.note.id);
|
||||
NoteListUtils.stopExternalEditing(this.state.note.id);
|
||||
}
|
||||
|
||||
async commandSetTags() {
|
||||
@@ -1328,7 +1342,7 @@ class NoteTextComponent extends React.Component {
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: this.state.note.id,
|
||||
noteIds: [this.state.note.id],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1392,7 +1406,7 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
|
||||
editorPasteText() {
|
||||
this.wrapSelectionWithStrings('', '', '', clipboard.readText());
|
||||
this.wrapSelectionWithStrings(clipboard.readText(), '', '', '');
|
||||
}
|
||||
|
||||
selectionRangePreviousLine() {
|
||||
@@ -1411,7 +1425,7 @@ class NoteTextComponent extends React.Component {
|
||||
return this.selectionRange_ ? this.rangeToTextOffsets(this.selectionRange_, this.state.note.body) : null;
|
||||
}
|
||||
|
||||
wrapSelectionWithStrings(string1, string2 = '', defaultText = '', replacementText = '') {
|
||||
wrapSelectionWithStrings(string1, string2 = '', defaultText = '', replacementText = null, byLine = false) {
|
||||
if (!this.rawEditor() || !this.state.note) return;
|
||||
|
||||
const selection = this.textOffsetSelection();
|
||||
@@ -1419,10 +1433,14 @@ class NoteTextComponent extends React.Component {
|
||||
let newBody = this.state.note.body;
|
||||
|
||||
if (selection && selection.start !== selection.end) {
|
||||
const s1 = this.state.note.body.substr(0, selection.start);
|
||||
const s2 = replacementText ? replacementText : this.state.note.body.substr(selection.start, selection.end - selection.start);
|
||||
const s3 = this.state.note.body.substr(selection.end);
|
||||
newBody = s1 + string1 + s2 + string2 + s3;
|
||||
const selectedLines = replacementText !== null ? replacementText : this.state.note.body.substr(selection.start, selection.end - selection.start);
|
||||
let selectedStrings = byLine ? selectedLines.split(/\r?\n/) : [selectedLines];
|
||||
|
||||
newBody = this.state.note.body.substr(0, selection.start);
|
||||
for (let i = 0; i < selectedStrings.length; i++) {
|
||||
newBody += string1 + selectedStrings[i] + string2;
|
||||
}
|
||||
newBody += this.state.note.body.substr(selection.end);
|
||||
|
||||
const r = this.selectionRange_;
|
||||
|
||||
@@ -1438,7 +1456,7 @@ class NoteTextComponent extends React.Component {
|
||||
column: r.end.column + str1Split[str1Split.length - 1].length },
|
||||
};
|
||||
|
||||
if (replacementText) {
|
||||
if (replacementText !== null) {
|
||||
const diff = replacementText.length - (selection.end - selection.start);
|
||||
newRange.end.column += diff;
|
||||
}
|
||||
@@ -1454,7 +1472,7 @@ class NoteTextComponent extends React.Component {
|
||||
editor.focus();
|
||||
});
|
||||
} else {
|
||||
let middleText = replacementText ? replacementText : defaultText;
|
||||
let middleText = replacementText !== null ? replacementText : defaultText;
|
||||
const textOffset = this.currentTextOffset();
|
||||
const s1 = this.state.note.body.substr(0, textOffset);
|
||||
const s2 = this.state.note.body.substr(textOffset);
|
||||
@@ -1526,26 +1544,30 @@ class NoteTextComponent extends React.Component {
|
||||
this.wrapSelectionWithStrings(TemplateUtils.render(value));
|
||||
}
|
||||
|
||||
addListItem(string1, string2 = '', defaultText = '') {
|
||||
const currentLine = this.selectionRangeCurrentLine();
|
||||
addListItem(string1, string2 = '', defaultText = '', byLine=false) {
|
||||
let newLine = '\n';
|
||||
if (!currentLine) newLine = '';
|
||||
this.wrapSelectionWithStrings(newLine + string1, string2, defaultText);
|
||||
const range = this.selectionRange_;
|
||||
if (!range || (range.start.row === range.end.row && !this.selectionRangeCurrentLine())) {
|
||||
newLine = '';
|
||||
}
|
||||
this.wrapSelectionWithStrings(newLine + string1, string2, defaultText, null, byLine);
|
||||
}
|
||||
|
||||
commandTextCheckbox() {
|
||||
this.addListItem('- [ ] ', '', _('List item'));
|
||||
this.addListItem('- [ ] ', '', _('List item'), true);
|
||||
}
|
||||
|
||||
commandTextListUl() {
|
||||
this.addListItem('- ', '', _('List item'));
|
||||
this.addListItem('- ', '', _('List item'), true);
|
||||
}
|
||||
|
||||
// Converting multiple lines to a numbered list will use the same number on each line
|
||||
// Not ideal, but the rendered text will still be correct.
|
||||
commandTextListOl() {
|
||||
let bulletNumber = markdownUtils.olLineNumber(this.selectionRangeCurrentLine());
|
||||
if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(this.selectionRangePreviousLine());
|
||||
if (!bulletNumber) bulletNumber = 0;
|
||||
this.addListItem(`${bulletNumber + 1}. `, '', _('List item'));
|
||||
this.addListItem(`${bulletNumber + 1}. `, '', _('List item'), true);
|
||||
}
|
||||
|
||||
commandTextHeading() {
|
||||
@@ -1825,6 +1847,7 @@ class NoteTextComponent extends React.Component {
|
||||
const menu = NoteListUtils.makeContextMenu(this.props.selectedNoteIds, {
|
||||
notes: this.props.notes,
|
||||
dispatch: this.props.dispatch,
|
||||
watchedNoteFiles: this.props.watchedNoteFiles,
|
||||
});
|
||||
|
||||
const buttonStyle = Object.assign({}, theme.buttonStyle, {
|
||||
@@ -1923,14 +1946,17 @@ class NoteTextComponent extends React.Component {
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
marginRight: rootStyle.paddingLeft,
|
||||
color: theme.color,
|
||||
color: theme.textStyle.color,
|
||||
fontSize: theme.textStyle.fontSize * 1.25 *1.5,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
fontSize: theme.fontSize,
|
||||
};
|
||||
|
||||
const toolbarStyle = {};
|
||||
const toolbarStyle = {
|
||||
marginTop: 3,
|
||||
marginBottom: 0,
|
||||
};
|
||||
|
||||
const tagStyle = {
|
||||
marginBottom: 10,
|
||||
@@ -1940,11 +1966,11 @@ class NoteTextComponent extends React.Component {
|
||||
const searchBarHeight = this.state.showLocalSearch ? 35 : 0;
|
||||
|
||||
let bottomRowHeight = 0;
|
||||
if (NOTE_TAG_BAR_FEATURE_ENABLED) {
|
||||
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - tagStyle.height - tagStyle.marginBottom;
|
||||
if (this.canDisplayTagBar()) {
|
||||
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginTop - toolbarStyle.marginBottom - tagStyle.height - tagStyle.marginBottom;
|
||||
} else {
|
||||
toolbarStyle.marginBottom = 10;
|
||||
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom;
|
||||
toolbarStyle.marginBottom = tagStyle.marginBottom,
|
||||
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginTop - toolbarStyle.marginBottom;
|
||||
}
|
||||
|
||||
bottomRowHeight -= searchBarHeight;
|
||||
@@ -2004,6 +2030,7 @@ class NoteTextComponent extends React.Component {
|
||||
if (htmlHasChanged) {
|
||||
let options = {
|
||||
pluginAssets: this.state.lastRenderPluginAssets,
|
||||
pluginAssetsHeadersHtml: assetsToHeaders(this.state.lastRenderPluginAssets),
|
||||
downloadResources: Setting.value('sync.resourceDownloadMode'),
|
||||
};
|
||||
this.webviewRef_.current.wrappedInstance.send('setHtml', html, options);
|
||||
@@ -2061,7 +2088,7 @@ class NoteTextComponent extends React.Component {
|
||||
/>
|
||||
);
|
||||
|
||||
const tagList = !NOTE_TAG_BAR_FEATURE_ENABLED ? null : <TagList style={tagStyle} items={this.state.noteTags} />;
|
||||
const tagList = this.canDisplayTagBar() ? <TagList style={tagStyle} items={this.state.noteTags} /> : null;
|
||||
|
||||
const titleBarMenuButton = (
|
||||
<IconButton
|
||||
@@ -2119,6 +2146,8 @@ class NoteTextComponent extends React.Component {
|
||||
onSelectionChange={this.aceEditor_selectionChange}
|
||||
onFocus={this.aceEditor_focus}
|
||||
readOnly={visiblePanes.indexOf('editor') < 0}
|
||||
// Enable/Disable the autoclosing braces
|
||||
setOptions={{ behavioursEnabled: Setting.value('editor.autoMatchingBraces') }}
|
||||
// Disable warning: "Automatically scrolling cursor into view after
|
||||
// selection change this will be disabled in the next version set
|
||||
// editor.$blockScrolling = Infinity to disable this message"
|
||||
@@ -2131,7 +2160,25 @@ class NoteTextComponent extends React.Component {
|
||||
/>
|
||||
);
|
||||
|
||||
const noteSearchBarComp = !this.state.showLocalSearch ? null : <NoteSearchBar ref={this.noteSearchBar_} style={{ display: 'flex', height: searchBarHeight, width: innerWidth, borderTop: `1px solid ${theme.dividerColor}` }} onChange={this.noteSearchBar_change} onNext={this.noteSearchBar_next} onPrevious={this.noteSearchBar_previous} onClose={this.noteSearchBar_close} />;
|
||||
const noteSearchBarComp = !this.state.showLocalSearch ? null : (
|
||||
<NoteSearchBar
|
||||
ref={this.noteSearchBar_}
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: searchBarHeight,
|
||||
width: innerWidth,
|
||||
borderTop: `1px solid ${theme.dividerColor}`,
|
||||
}}
|
||||
query={this.state.localSearch.query}
|
||||
searching={this.state.localSearch.searching}
|
||||
resultCount={this.state.localSearch.resultCount}
|
||||
selectedIndex={this.state.localSearch.selectedIndex}
|
||||
onChange={this.noteSearchBar_change}
|
||||
onNext={this.noteSearchBar_next}
|
||||
onPrevious={this.noteSearchBar_previous}
|
||||
onClose={this.noteSearchBar_close}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={rootStyle} onDrop={this.onDrop_}>
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
@@ -68,7 +70,7 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<Header style={headerStyle}/>
|
||||
<div style={{padding: 10}}>
|
||||
<div style={{ padding: 10 }}>
|
||||
{logComps}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { render } = require('react-dom');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import JoplinServerApi from '../lib/JoplinServerApi';
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
|
||||
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const shared = require('lib/components/shared/side-menu-shared.js');
|
||||
@@ -678,6 +680,11 @@ class SideBarComponent extends React.Component {
|
||||
id: selectedItem.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (keyCode === 65 && (event.ctrlKey || event.metaKey)) {
|
||||
// Ctrl+A key
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onHeaderClick_(key, event) {
|
||||
|
@@ -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');
|
||||
|
@@ -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 { connect } = require('react-redux');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -16,7 +18,7 @@ class TagListComponent extends React.Component {
|
||||
style.fontSize = theme.fontSize;
|
||||
|
||||
const tagItems = [];
|
||||
if (tags || tags.length > 0) {
|
||||
if (tags && tags.length > 0) {
|
||||
// Sort by id for now, but probably needs to be changed in the future.
|
||||
tags.sort((a, b) => {
|
||||
return a.title < b.title ? -1 : +1;
|
||||
@@ -31,10 +33,6 @@ class TagListComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (tagItems.length === 0) {
|
||||
style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tag-list" style={style}>
|
||||
{tagItems}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user