1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

...

141 Commits

Author SHA1 Message Date
Laurent Cozic
49cb391486 Electron release v1.0.183 2020-02-07 23:27:27 +00:00
Laurent Cozic
cfdde4c2ce Removed package.json comments as it breaks CI 2020-02-07 23:27:16 +00:00
Laurent Cozic
57864a388a Electron release v1.0.182 2020-02-07 23:24:00 +00:00
Laurent Cozic
74c8a38d48 CLI v1.0.154 2020-02-07 23:23:30 +00:00
Laurent Cozic
cd19cedd46 Moved joplin-renderer dependencies to client package.json as a workaround to electron-builder bug
Due to this bug:

https://github.com/electron-userland/electron-builder/issues/3185
2020-02-07 23:21:16 +00:00
Helmut K. C. Tessarek
3f23d8ed06 Desktop, Cli: Fixes #2085: Fix escaping of title when generating a markdown link (#2456)
Previously a title with brackets was escaped incorrectly. The brackets were replaced by underscores.

The following title `title [square] (round)` looked like this:

[title _square_ _round_](:/c54794f53e5e4b1aa558699e255d5f95)

Now it looks like this:

[title \[square\] (round)](:/c54794f53e5e4b1aa558699e255d5f95)

fixes #2085
2020-02-07 22:15:41 +00:00
Helmut K. C. Tessarek
8cbb0d03e8 fix de_DE.po 2020-02-07 16:31:47 -05:00
Tomáš Bambas
52b99a1520 Translation: Update cs_CZ.po (#2462) 2020-02-07 16:07:22 -05:00
Helmut K. C. Tessarek
7eabe74402 Cli: Fix console messages being displayed in GUI (#2457)
see https://discourse.joplinapp.org/t/joplin-terminal-question-sync/5700?u=tessus
2020-02-07 09:49:47 +00:00
Laurent Cozic
10cf80d6ca Merge branch 'master' of github.com:laurent22/joplin 2020-02-06 11:55:35 +00:00
Helmut K. C. Tessarek
bccfd0bcbd Desktop: Sort tags in drop-down list (when adding tags) (#2453)
see https://discourse.joplinapp.org/t/request-better-tag-organisation/5662?u=tessus
2020-02-06 10:51:24 +00:00
mic704b
fa9e2bd6dd Desktop: Support scrolling in the note list using keys (eg page up, page down) (#2404)
* Implement note list navigation: page up/down and home/end.

* Adjust key code mappings.

* Refactor.

* Add comments to clarify key codes.

* Fix formatting.
2020-02-06 09:38:33 +00:00
mic704b
b15b3d6ac5 Desktop: Do not select pasted text no matter the paste method (#2431) 2020-02-06 01:38:17 -05:00
Laurent Cozic
c4fb5b72cd Electron release v1.0.181 2020-02-05 22:34:21 +00:00
Laurent Cozic
fd706c3dbc Trying to revert to electron-builder 20.15.0 to go around build issue 2020-02-05 22:34:13 +00:00
mic704b
5128190942 Desktop: Resolves #2330: Fix rendering of tabs in code blocks (#2446)
* Add renderer plugin to handle tabs in code blocks.

* Add plugin to renderer package list.

* Attempt to fix unrelated linter issues.

* Fix unrelated linter problems.

The problems exist on master prior to the branch.

* Fix more inherited linter problems.
2020-02-05 22:15:40 +00:00
mic704b
c6f127b48e Desktop: Fixes #2407: Do not show "could not print" warning dialog after cancelling print. (#2410)
* Do not show "failed to print" warning dialog after cancelling print.

* Add reason to error string.

* Reform message string.

* Ensure OK button is displayed on error dialog.
2020-02-05 21:50:05 +00:00
mic704b
011d66356f Desktop: Resolves #1014: Support list creation on multi-line selections (#2408)
* Support multi-line selections for creating ordered lists, unordered lists, checkboxes.

* Modify to maintain previous behaviour wrt insertion of new line if not on empty line.

* Review update: rename variables (or eliminate them).

* Review update: variable naming.
2020-02-05 21:38:55 +00:00
Amit singh
d24a974219 Desktop: fixes long lines warpped (#2447) 2020-02-05 21:35:37 +00:00
mic704b
82f5e26ef4 Desktop: Resolves #539: Add "add or remove tags" for multiple notes. (#2386)
* Add `add or remove tags` for multiple notes.

* Fix test.

* Handle invalid argument.

* Enable "Edit > Tag" menu item.

* Clean up variable naming.
2020-02-05 21:24:12 +00:00
0xCLOVER
247182edbf Desktop: Fix #2365: Ensure the main window is hidden when Joplin starts (#2432)
* Ensure window is hidden when application launches

* Update ElectronAppWrapper.js

Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2020-02-05 11:26:57 +00:00
Vaidotas Simkus
69fc518e39 Desktop: Allow --no-sandbox flag to go around AppImage limitation (#2436) 2020-02-05 11:24:09 +00:00
Andrey Dolgov
aef4a88d7f All: Reset time fields when duplicating a note (#2428) 2020-02-05 11:18:14 +00:00
mic704b
69e70d88f4 Desktop: Improved Note search bar UI (#2329)
* Add colour hints to the local search bar.

* Refactor.

* Refactor.

* Fix annoying flicker when entering first query character.

* Refactor in response to review comments.

Cache the information at the source, remove state updates during render.

* Move cached data into searchbar component.

* Refactor.

* Show number of matches and disable prev/next buttons when no matches.

* Improve no matches message.

* More note searchbar enhancements.

Indicate selected match
Fade search result text
Ctrl-F selects input content upon repeat

* Update following review.

Modify message to remove need for translation.
Flatten properties structure.

* Made tweaks to avoid having two queries in the state

* Cache searchbox background colour to stop compoenent flashing.

* Update NoteSearchBar.jsx

Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2020-02-05 10:45:24 +00:00
Laurent Cozic
aac0a45beb Merge branch 'enhance-note-searchbox' of https://github.com/mic704b/joplin 2020-02-05 10:38:16 +00:00
mic704b
d04d89d622 Desktop: Emphasise note title (#2311)
* Emphasise note title and de-emphasise note toolbar.

* Remove redundant titles from pre-packaged notes.

* Revert additions to theme.

* Revert de-emphasis of note toolbar icons.

* Revert bolding of title.

* Undo change to theme

* Use h1 style as basis for title editor style.

Times a factor to make up for html scaling.

* Use text style as basis for editor title style, scaled to match html h1.
2020-02-05 10:37:26 +00:00
Laurent Cozic
0f1633dfbf Tools: Linter: Enforce array-bracket-spacing - never 2020-02-04 22:11:35 +00:00
Laurent Cozic
737c3f62db Tools: Linter: Enforce object-curly-spacing - always 2020-02-04 22:09:34 +00:00
fab4x
49701fbc55 Translation: update de_DE.po (#2443)
* update de_DE.po

Added some translations

* Update German translation 

Added some translations

* deletion
2020-02-04 16:56:11 -05:00
mic704b
5395d57df8 Desktop: Support "select all" in the note list (#2403)
* Select all notes in note list, block select all in folder and tags lists.

* Adjust key mappings.

* Adjust key mappings.
2020-02-04 21:55:05 +00:00
Laurent Cozic
8a7e3fe36f Update website 2020-02-04 12:49:21 +00:00
Laurent Cozic
7bc0a52cc9 Doc: GSoC: Added Roeland Jago Douma from Nextcloud as mentor 2020-02-04 12:48:34 +00:00
mic704b
f428cc26a2 All: More info for "unknown profile version" error message (#2361)
* Add version info to profile error message. Add profile version to desktop About Box.

* Add profile error to log.

* Use shim to retrive version number.

* Refactor to use registry instead of BaseModel to get database.

* Remove call to logger.

* Improve code readability.
2020-02-03 21:40:48 +00:00
Diego Betto
cf6c141e57 version attribute inside joplin.desktop is X-AppImage-Version (#2393)
- version attribute inside `joplin.desktop` is `X-AppImage-Version` and not `X-AppImage-BuildId`
- format is `X-AppImage-Version=X.Y.Z" so we need to change `cut` parameters (split on '=' and get 2nd value)
2020-02-03 21:38:52 +00:00
mic704b
3a29b5f321 Update tools build command to end in directory from which it started. (#2434) 2020-02-03 19:59:12 +00:00
chenlhlinux
ffdae41605 Translation: Update zh_CN.po (#2421) 2020-01-31 18:41:04 -05:00
Laurent Cozic
688edd4b32 Moved the Joplin renderer back to the main repository to make development easier 2020-01-30 21:05:23 +00:00
Laurent Cozic
d687ef5c09 Merge branch 'master' of github.com:laurent22/joplin 2020-01-31 09:48:54 +00:00
Marton Paulo
59c8a87047 Remove personal e-mail address eo.po pt_BR.po (#2414)
* Remove personal e-mail address

* Remove personal e-mail address

* Remove personal e-mail address

* Revert "Remove personal e-mail address"

This reverts commit 7a66c4a5b6.

* Remove space

* Remove space
2020-01-30 15:55:23 -05:00
Laurent Cozic
759d59c2e6 Tools: Ignore existing components for react-hook rule 2020-01-29 18:03:55 +00:00
Laurent Cozic
73d12e1ed5 Tools: Enforce React hooks on linter 2020-01-29 17:54:25 +00:00
Kirtan Purohit
fcda843778 All: Resolves #2279: Handle Thai language in search (#2387)
* Update SearchEngine.js

use basicSearch if thai string

* Update string-utils.js

added thai regex support

* Update services_SearchEngine.js

added thai language test

* Update services_SearchEngine.js

remove trailing spaces
2020-01-29 12:50:52 +00:00
Helmut K. C. Tessarek
dcbd8aed30 clarify 2 paragraphs in CONTRIBUTING.md 2020-01-27 19:49:36 -05:00
Helmut K. C. Tessarek
154c838e9f dynamically retrieve copyright year in web site generation 2020-01-27 19:25:10 -05:00
Helmut K. C. Tessarek
f90f688299 fix the 'Improve this doc' link on the index (main) page
The 'improve this doc' link results in a 404 on the main page, because no filename is actually given for the index page.
2020-01-27 19:06:54 -05:00
Helmut K. C. Tessarek
fca9b57af5 Update website 2020-01-27 19:00:52 -05:00
Helmut K. C. Tessarek
59eed8395d fix copyright year in web site generation 2020-01-27 18:56:33 -05:00
Helmut K. C. Tessarek
a4ccd2d43a fix print function in installer script
fixes #2379
2020-01-27 03:40:29 -05:00
Helmut K. C. Tessarek
5136e7a0e0 Merge branch 'master' of github.com:laurent22/joplin 2020-01-27 03:30:09 -05:00
Helmut K. C. Tessarek
423243c84b clarify bug reports and feature requests in the community section 2020-01-27 03:29:52 -05:00
Runo Saduwa
2042deb2bf Documentation: Edit build instructions to avoid confusion (#2376)
* Edits build instructions to avoid confusion

When i was trying to build the Electron Project, i was a bit confused about which commands to follow for building in the projects on my OS (Windows).
there was two header  **Building the Electron application** and   **Building the Electron application for windows**, the former can lead a beginner to confusion because it seems to be "a generalized statement", unlike the latter that was more specific. In order to improve the developers experience, i had to change the former heading to "Building the Electron application for Linux and macOS" to help them understand easily.

* Implemented advises from Reviewer (tessus) to further improve  the documentation instructions for building the Electron App on Linux, macOS and Windows
2020-01-27 03:16:12 -05:00
Helmut K. C. Tessarek
e1216dce4b Update en_US.po 2020-01-26 23:19:54 -05:00
Helmut K. C. Tessarek
3839c7818e Update de_DE.po 2020-01-26 23:19:37 -05:00
abonte
90652e40b4 Translation: Update it_IT.po (#2366)
* Update Italian translations

* fix double quotes
2020-01-26 19:56:06 -05:00
mic704b
83c1c20ce3 Desktop: Fix hang when selecting tag when multiple notes are selected (also for search) (#2372)
* Fix hang when tag clicked while multiple notes selected.

* Fix hang when search changed while multiple notes selected from previous search.
2020-01-26 17:46:19 +00:00
Helmut K. C. Tessarek
0bb1484b2d Deskop: Fix deprecation of getName() in Electron (#2367)
This change fixes the following deprecation:
(electron) 'getName function' is deprecated and will be removed. Please use 'name property' instead.
2020-01-26 17:32:00 +00:00
XSAkos
5881cee167 All: Added new date format YYYY.MM.DD (#2318)
* Added new date format YYYY.MM.DD.

* revert change as this file is autogenerated

Co-authored-by: Akos Keresztes <60130238+keresztesa@users.noreply.github.com>
2020-01-26 17:26:50 +00:00
Laurent Cozic
101935e594 Tools: Allow creating simplified changelog for app stores 2020-01-25 10:42:36 +00:00
Laurent Cozic
9dda65de20 Merge branch 'master' of github.com:laurent22/joplin 2020-01-25 10:28:57 +00:00
Laurent Cozic
a00e35fb57 ios-v10.0.43 2020-01-25 10:28:22 +00:00
mic074b
2c85b55ff8 Cache searchbox background colour to stop compoenent flashing. 2020-01-25 15:30:57 +11:00
Laurent Cozic
d1b51b409a Made tweaks to avoid having two queries in the state 2020-01-25 15:29:48 +11:00
Laurent Cozic
c3d5463589 Tools: Improved CLI release script 2020-01-24 23:18:46 +00:00
Laurent Cozic
710447f879 Merge branch 'master' of github.com:laurent22/joplin 2020-01-24 23:18:02 +00:00
Laurent Cozic
c61e4cae4d CLI v1.0.153 2020-01-24 23:17:46 +00:00
Laurent Cozic
333aebf32c Tools: Removed debugging code 2020-01-24 23:15:05 +00:00
Laurent Cozic
2657c8736e Tools: Fixes #2256: Better handling of git failure when building package-info 2020-01-24 23:10:24 +00:00
Laurent Cozic
5b28f6b25f Tools: Improved git changelog 2020-01-24 22:43:55 +00:00
Laurent Cozic
715253da2f Android release v1.0.316 2020-01-24 22:34:02 +00:00
Laurent Cozic
66356d83ab Electron release v1.0.179 2020-01-24 22:29:17 +00:00
Laurent Cozic
8e531ca87a Update translations 2020-01-24 22:28:54 +00:00
Laurent Cozic
18c46851fd Desktop: Fixes #2324: Apply userstyle again when exporting to PDF or printing 2020-01-24 21:46:48 +00:00
Laurent Cozic
5456dbbf16 Merge branch 'master' of github.com:laurent22/joplin 2020-01-24 21:16:33 +00:00
Laurent Cozic
5c54b83108 Desktop: Fixed update message box 2020-01-24 21:16:25 +00:00
Laurent Cozic
cbf7f03bff Desktop: Improve appearance of note Info dialog 2020-01-24 21:16:18 +00:00
mic704b
ea05fea234 Desktop: Fix identification of note in pdf export from main menu. (#2323) 2020-01-24 20:57:11 +00:00
mic704b
f78729ad1f Cross-platform version number retrieval (#2355)
* Add shim to retrieve application version.

* Create shim to retrieve app version number.

* Add a fall through handler to throw an error.
2020-01-24 20:56:44 +00:00
genneko
4ec9492f7c Update ja_JP.po (#2358) 2020-01-24 15:19:57 +00:00
bedwardly-down
f86b953420 Tools: Updated Request to v2.88.0 to remove dependency on Outdated and Insecure Hawk and Cryptiles Dependencies (#2353)
* Updated Cryptiles dependency to 3.1.4

The maintainer is no longer supporting any version of cryptiles before
v4.2.0 on npm; npm's older versions stop at 3.1.2 and a high security
vulnerability was thrown during a build on linux: https://npmjs.com/advisories/720

The maintainer still has 3.1.4 tagged in his repo and this commit
updates cryptiles to solve that issue without breaking compatibility
since it was primarily bug fixes for a code freeze.

* Updated Request Dependency to 2.86.0

* Updated Reqiest to 2.88.0
2020-01-24 15:19:22 +00:00
mic074b
d8f91a2ece Update following review.
Modify message to remove need for translation.
Flatten properties structure.
2020-01-24 19:23:01 +11:00
Laurent Cozic
6563606799 Merge branch 'master' of github.com:laurent22/joplin 2020-01-23 17:43:16 +00:00
Laurent Cozic
c01bc1c363 All: Added new, more secure encryption methods, so that they can be switched to at a later time 2020-01-22 22:01:58 +00:00
Laurent Cozic
6f8c634756 Tools: Add developer names to changelog 2020-01-22 20:33:43 +00:00
Alexander Pankratov
22a93994aa Translation: Update ru_RU.po (#2347) 2020-01-23 01:07:12 -05:00
Xaris Ar
e0013858c4 Translation: Update el_GR.po (#2340)
* Create el_GR.po (part1)

* Update el_GR.po

* Update el_GR.po (part 2)

* Update el_GR.po

* Finished Greek(el_GR) translation

Finished translating all texts.
Update el_GR.po (beta)

* Update el_GR.po

to meet requirements for Joplin 1.0.173

* Update locale.js

* Delete el_GR.po

* Create el_GR.po

* Update Greek Translation

* Update Greek Translation
2020-01-22 22:31:19 -05:00
Laurent Cozic
b6e0df57eb Desktop: Fixes #2352: undefined text was being displayed on top of notes in revision viewer 2020-01-22 17:32:21 +00:00
Laurent Cozic
be210233be Desktop, Mobile: Fixes #2339, Fixes #2343, Fixes #2345: Fixed issues with Katex and MultiMd table plugin 2020-01-22 17:16:37 +00:00
Carl Bordum Hansen
1a1a1d3841 Desktop: Fix Linux installation script (#2333) 2020-01-22 13:47:20 +00:00
Laurent Cozic
4283bbde7f Doc: Fixed changelog generation for API 2020-01-21 10:40:29 +00:00
Laurent Cozic
fba325f60e Doc: Fixed APIdoc 2020-01-21 09:44:46 +00:00
Laurent Cozic
fcd76dabac Update website 2020-01-21 09:42:27 +00:00
mic074b
f661cad6a3 More note searchbar enhancements.
Indicate selected match
Fade search result text
Ctrl-F selects input content upon repeat
2020-01-21 20:06:28 +11:00
mic074b
1faac68441 Merge branch 'enhance-search-box' into enhance-search-box-extras 2020-01-21 18:45:08 +11:00
Helmut K. C. Tessarek
e9366a0d41 Update translations 2020-01-20 19:11:57 -05:00
Laurent Cozic
953aa5d0b5 Electron release v1.0.178 2020-01-20 18:44:23 +00:00
mic074b
fc5782990f Improve no matches message. 2020-01-20 22:27:38 +11:00
mic074b
01163783ef Show number of matches and disable prev/next buttons when no matches. 2020-01-20 22:13:02 +11:00
mic074b
be19a92f59 Refactor. 2020-01-20 21:25:28 +11:00
Laurent Cozic
3fed1abc36 API: Add ability to search by folder or tag title 2020-01-20 02:19:57 +00:00
Alexander Teterkin
6973bf9331 Documentation: Added 'Yandex Disk' to the list of WebDAV-compatible services known to work with Joplin. (#2285)
* Add Yandex Disk to the list of WebDAV-compatible services

Yandex Disk (cloud storage by Yandex) supports WebDAV access and is known to work with Joplin.

* Add Yandex Disk to the list of WebDAV-compatible services

Added 'Yandex Disk' to the list of WebDAV-compatible services known to work with Joplin (now in correct alphabetical order).
2020-01-20 01:02:54 -05:00
Laurent Cozic
e8867fa0f1 Doc: Added CalebJohn as mentor on three ideas 2020-01-19 16:24:13 +00:00
Bart
d9c15b84d0 Desktop: when importing MD files create resources for local linked files (#2262)
* md importer: first pass import attachment resources with markdown files

* md importer: import resources from md - no unneeded saves, check if files exist, regex name

* md importer: test import of local files as resources, separate method for importing linked files, comment regex matching md tags

* md importer: move stateful regex to method scope, remove spurius await

* md importer: lint

* md importer: respond to PR comments: remove test nesting, test sample, check if path is dir, use shim.fsDriver

* md importer: use file-path methods for getting attachment path

* md importer: use extractImageUrls helper, test for file with zero links

* md importer: try catch around importLocalImages, improve test

* md importer: importing attached images cover case where link also appears elsewhere in doc

* md importer: only create 1 resource if note contains duplicate links, test

* md importer: remove log

* md importer: remove use of lodash
2020-01-19 15:39:38 +00:00
Helmut K. C. Tessarek
81876c7bf3 Desktop: Update Electron to 7.1.9 (#2314)
This is only a minor bump but several issues have been fixed since 7.1.5.

The most pressing one that we've experienced for a long time:

default button in dialog not working on macOS
https://github.com/electron/electron/issues/21633
2020-01-19 15:34:45 +00:00
mic074b
ce6c7c8783 Move cached data into searchbar component. 2020-01-19 11:18:52 +11:00
mic074b
fad2ff674e Refactor in response to review comments.
Cache the information at the source, remove state updates during render.
2020-01-19 10:51:15 +11:00
Laurent Cozic
1dd7727e97 Doc: Fixed GitHub icon 2020-01-18 15:06:35 +00:00
Laurent Cozic
fe0318584e Update website 2020-01-18 14:54:10 +00:00
Laurent Cozic
8508fe737b Doc: Added GitHub Sponsor icon 2020-01-18 14:53:49 +00:00
Laurent Cozic
c7a9e5f656 Merge branch 'master' of github.com:laurent22/joplin 2020-01-18 14:27:26 +00:00
Laurent Cozic
3e43fbce13 Doc: Fix GSoC doc 2020-01-18 14:27:21 +00:00
mic704b
b304e2ae1f Desktop: Fix bug in note tags display due to error in comparison of tag lists. (#2302) 2020-01-18 13:55:35 +00:00
lightray22
35f4ede11a Desktop: show completed date in note properties (#2292) 2020-01-18 13:53:59 +00:00
mic704b
65cbb6e388 Desktop: Maintain selection when non-selected note is deleted (#2290)
* Fix jump of focus following deletion action.

Applies to notes, folders and tags.

* Add tests for reducer item delete handling.

* Add comments.

* Clean up.
2020-01-18 13:53:00 +00:00
lightray22
960d7f84eb Desktop: Don't count completed to-dos in note counts when they are not shown (#2288)
* Desktop: don't count completed to-dos in note counts when they are not shown

* Desktop: review comments for commit 0383dcc

* Desktop: fix remaining lint issues with commit 1fe4685
2020-01-18 13:46:04 +00:00
mic704b
8a392e1c06 Desktop: Fixes #2254: Fix pdf export when mouse over non-selected note in notelist. (#2255) 2020-01-18 13:30:15 +00:00
Vaidotas Simkus
d9d75d6c71 Desktop, Cli: Replace note links with relative paths in MD Exporter (#2161)
* Replace linked Note ids by relative paths in MD Exporter.

* Added tests for the MD Exporter.

* Changed fs.readdirSync use for earlier Node version (v8)

In the previous commit the code used fs.readdirSync from Node v10 or
later. But since Joplin still uses v8, I changed the use of
fs.readdirSync to be in line with the earlier api.

* Updated readDirSync use for Node v10, which allows gets folder names too.

* Revert "Updated readDirSync use for Node v10, which allows gets folder names too."

This reverts commit 8f255db120861dd7773d99e1b63f4864d39594cf.
Because the Travis builds still use Node v8. This is fine as well, the
readdirSync returns the filenames in the directory.

* Added reservedNames param to findUniqueFilename
2020-01-18 13:16:14 +00:00
Laurent Cozic
69f9e38730 Doc: Improved test unit doc 2020-01-18 13:11:42 +00:00
Laurent Cozic
7f95186a97 Doc: Updated GSoC ideas and added section about abandoned pull requests 2020-01-18 13:08:23 +00:00
mic074b
b6db2bf2c5 Fix annoying flicker when entering first query character. 2020-01-18 14:16:12 +11:00
Laurent Cozic
6f976abf42 Doc: Added "Custom keyboard shortcuts" idea for GSoC 2020-01-17 10:53:53 +00:00
Laurent Cozic
d80ffeeba1 Doc: Update mentors for GSoC 2020-01-17 10:48:35 +00:00
mic074b
c856e8d9ac Refactor. 2020-01-17 17:13:28 +11:00
mic074b
6736bda429 Refactor. 2020-01-17 17:08:41 +11:00
mic074b
0a8f9163db Add colour hints to the local search bar. 2020-01-16 23:02:36 +11:00
zen-quo
e078de25f0 Translation: Update tr_TR.po (#2295) 2020-01-12 19:01:00 -05:00
Laurent Cozic
cd284f78ad Update website 2020-01-11 16:58:32 +00:00
Laurent Cozic
0a13c988fa Mobile: Fixes #1816: When creating a new note, it was not possible to focus the body text field 2020-01-08 17:42:28 +00:00
Laurent Cozic
b61bfd6ffe Android: Fixes #2270: Note files could become corrupted when using file system sync on certain Android versions 2020-01-08 18:57:40 +00:00
Laurent Cozic
fc61b474cd Clipper: Fixes #2252: Some pages that contain tables with only one cell would trigger an error 2020-01-08 18:35:41 +00:00
Laurent Cozic
bf25364333 Clipper: Fixes #2267: Fixed race condition when importing page that have multiple images with similar names 2020-01-08 18:21:13 +00:00
Laurent Cozic
bc7099d29b Desktop, Mobile: Fixed regression in HTML note rendering 2020-01-08 18:05:13 +00:00
Laurent Cozic
00c3ed715c Update PT translation 2020-01-07 15:44:54 +00:00
Caleb John
701b57de89 Fix casing typo for PluginAssetsLoader.js in gitignore (#2250)
* Fix casing typo for PluginAssetsLoader.js in gitignore

* Add change to eslintignore
2020-01-07 00:47:51 +00:00
Caleb John
e674d7d23b Desktop: Add option to disable auto-matching braces (#2251)
* Add option to disable auto-matching braces

* Only Make option desktop only
2020-01-06 22:27:37 +00:00
Devon Zuegel
4a2d9bb028 CLI: Upgrade sqlite (#2248)
* Upgrade sqlite

* Add info about sqlite installation to docs
2020-01-06 22:24:38 +00:00
Abijeet Patro
ae3a278ac4 Desktop: New: Display selected tags under a note title (#2217)
Follow up to #893

Now using middleware to set the tags when a note is selected

This avoids the ugly code in the NoteTextComponent where we determine
if tags are to be fetched, identify if they have been modified, fetch
them  and then dispatch an action to update the store which might
again re-render the component.

Also implements style related fixes from #1000

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
Fixes: #469
2020-01-06 21:23:22 +00:00
mic704b
42ada7123c Desktop: Add external editor actions to the note context menu. (#2214)
* Add external editor actions to the note context menu.

Also start up external editor on note double click.

These changes enhance user experience by placing the actions where
they feel natural.

* Remove double-click behaviour and change menu text.

Changes in response to review comments.

* Move handling of external editor actions to main screen from note text

This is to ensure correct behaviour even when the user launches the
action on a note in the list that is under the pointer, but not selected.

* Move external edit actions to NoteListUtils from MainScreen.

* Reconnect external edit action in main edit menu.
2020-01-06 21:16:39 +00:00
Laurent Cozic
6d9f73eef7 Improved integration of external renderer 2019-12-30 21:54:13 +01:00
Laurent Cozic
541372eb91 Merge branch 'master' of github.com:laurent22/joplin 2019-12-30 20:54:53 +01:00
Laurent Cozic
8d7d70bc13 Desktop: Fixed export to HTML, PDF and printing 2019-12-30 20:44:15 +01:00
Scott Bronson
e77cc18468 Documentation: Fix broken link (#2245)
The folder is `gsoc2020`, not just `gsoc`
2019-12-30 12:42:44 -05:00
331 changed files with 40592 additions and 30924 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -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

View File

@@ -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!

View File

@@ -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];

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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"

View File

@@ -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
View 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 = '![does not exist](does_not_exist.png)';
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.');
});
});

View File

@@ -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');
// }));
});

View File

@@ -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);
}
}));
});

View File

@@ -0,0 +1,2 @@
![link 1](../support/photo.jpg)
![link 2](../support/photo.jpg)

View File

@@ -0,0 +1,3 @@
# Markdown
![../support/photo.jpg](../support/photo.jpg) should put resource link inside () not []
![../support/photo.jpg]( ../support/photo.jpg ) this case (spaces before/after link but within parens) is not currently covered

View 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.

View File

@@ -0,0 +1,13 @@
# Markdown
lorem ipsum ![alt text here](../support/photo.jpg)
- [ ] check!
- [ ] boxes!
![alt text here](../support/photo-two.jpg)ipsum lorem
**strong text**
![does not exist](does_not_exist.png) lorem ipsum
**some directory**
![a directory](../support) lorem ipsum

View File

@@ -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);
}));
});

View File

@@ -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);
}));
});

View File

@@ -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
View 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);
}));
});

View 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');
}));
});

View File

@@ -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: '我是法国人' });

View File

@@ -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);
}));

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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>;

View File

@@ -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', () => {

View File

@@ -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({

View File

@@ -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');

View File

@@ -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')],
});
}

View File

@@ -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);
}

View File

@@ -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 };

View File

@@ -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>

View File

@@ -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');

View File

@@ -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';

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -1,3 +1,5 @@
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
const React = require('react');
const { themeStyle } = require('../theme.js');

View File

@@ -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');

View File

@@ -1,3 +1,5 @@
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
const React = require('react');
class ItemList extends React.Component {

View File

@@ -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 });
},

View File

@@ -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');

View File

@@ -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) {

View File

@@ -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]);
}

View File

@@ -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}>

View File

@@ -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>
);

View File

@@ -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');

View File

@@ -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_}>

View File

@@ -1,3 +1,5 @@
/* eslint-disable enforce-react-hooks/enforce-react-hooks */
const React = require('react');
const { connect } = require('react-redux');

View File

@@ -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>

View File

@@ -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');

View File

@@ -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');

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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');

View File

@@ -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');

View File

@@ -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