Compare commits
341 Commits
cli-v3.5.1
...
android-v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
620afdaab1 | ||
|
|
3f8928000e | ||
|
|
5caec161f2 | ||
|
|
daab2223e7 | ||
|
|
f96071870c | ||
|
|
5e08abb7a9 | ||
|
|
2c71557d88 | ||
|
|
d551963669 | ||
|
|
7dae90c9f3 | ||
|
|
46820fb21b | ||
|
|
a18e49ab54 | ||
|
|
2c6eaca442 | ||
|
|
44de1246d9 | ||
|
|
ab3a0ab69f | ||
|
|
896f0e0bc5 | ||
|
|
e2c933db82 | ||
|
|
30c5031611 | ||
|
|
e7f14a0995 | ||
|
|
319bf79bc1 | ||
|
|
02f94adb96 | ||
|
|
2370c12129 | ||
|
|
8d074a563b | ||
|
|
1014edfdeb | ||
|
|
364bdd9bb0 | ||
|
|
8d6b219191 | ||
|
|
2455245f86 | ||
|
|
c669a3986e | ||
|
|
5f1a1e50d9 | ||
|
|
819a591cc0 | ||
|
|
421b82c86d | ||
|
|
16169b2780 | ||
|
|
49ed4ae920 | ||
|
|
13777d261c | ||
|
|
1c7b0e6266 | ||
|
|
4589670126 | ||
|
|
b6ab6e0b46 | ||
|
|
9b28b618bb | ||
|
|
bf7cc6be03 | ||
|
|
e5e5b342a7 | ||
|
|
9709721a73 | ||
|
|
a34010ef62 | ||
|
|
9a6043e6a6 | ||
|
|
992bf683c4 | ||
|
|
b40c2b8a41 | ||
|
|
8dcd08e21d | ||
|
|
cb2b32520d | ||
|
|
315b1d8275 | ||
|
|
8018f1269a | ||
|
|
c2d186188b | ||
|
|
d5798e558b | ||
|
|
224bcd54f1 | ||
|
|
1a3d572498 | ||
|
|
848a2c986a | ||
|
|
fc61a2bc6a | ||
|
|
f9d58742c0 | ||
|
|
5ba8cefe7c | ||
|
|
74484f194e | ||
|
|
eae569aff8 | ||
|
|
8734bc8467 | ||
|
|
612d09d16f | ||
|
|
eb2e9419b9 | ||
|
|
17935458e6 | ||
|
|
a69a5d98ee | ||
|
|
48c9c1112c | ||
|
|
a6585a67d0 | ||
|
|
959e1522d4 | ||
|
|
8605e5aad5 | ||
|
|
88af5208f5 | ||
|
|
bef73dbbf5 | ||
|
|
b23c50cc7d | ||
|
|
3e90a9392d | ||
|
|
e2a32c5993 | ||
|
|
759761086d | ||
|
|
ca29ed94cc | ||
|
|
f815933ad0 | ||
|
|
67af879d38 | ||
|
|
2e310e0f79 | ||
|
|
e63041766f | ||
|
|
93997277b6 | ||
|
|
4afac412ce | ||
|
|
b79bf11680 | ||
|
|
10d727f183 | ||
|
|
50e2dc7749 | ||
|
|
5108fe5b24 | ||
|
|
3536a68cfe | ||
|
|
d94d057f1d | ||
|
|
8ec11bddc2 | ||
|
|
4813c79b35 | ||
|
|
7778a68764 | ||
|
|
503e748ca8 | ||
|
|
b6297b609e | ||
|
|
31d37b30b0 | ||
|
|
0ccd7e474d | ||
|
|
046cfece32 | ||
|
|
0280bb80b9 | ||
|
|
8a61f4ec54 | ||
|
|
d7dd16aac1 | ||
|
|
e1ed573c33 | ||
|
|
b6c8347549 | ||
|
|
b150d6453d | ||
|
|
9feba9345d | ||
|
|
7fa3a3b545 | ||
|
|
fed2438bc3 | ||
|
|
31cb404854 | ||
|
|
dba3a3f68f | ||
|
|
14f8f51cd1 | ||
|
|
2240cf77b5 | ||
|
|
599f7a24ce | ||
|
|
f177563c4a | ||
|
|
a0bdc1fa9b | ||
|
|
f566e5c336 | ||
|
|
87d07eff4a | ||
|
|
3caf41984f | ||
|
|
7a31f1f156 | ||
|
|
090c1d9706 | ||
|
|
5e2b79557c | ||
|
|
74fa2a6eb9 | ||
|
|
791668455e | ||
|
|
91aedc5efa | ||
|
|
6b2d9ba5ec | ||
|
|
d8920840f2 | ||
|
|
bf571c5961 | ||
|
|
a7b22edbc4 | ||
|
|
f4904d8155 | ||
|
|
fab633bbb4 | ||
|
|
cda4073bfc | ||
|
|
903edb8fa2 | ||
|
|
f3409600e1 | ||
|
|
9f36b44842 | ||
|
|
6f41234db3 | ||
|
|
2feebf504e | ||
|
|
3312e96b0d | ||
|
|
af5108d702 | ||
|
|
0f4877f263 | ||
|
|
46c22fffb9 | ||
|
|
ae5bc1b849 | ||
|
|
907da6caa9 | ||
|
|
57a4a687d1 | ||
|
|
865d39d657 | ||
|
|
00aecd63d4 | ||
|
|
bd569b9d8d | ||
|
|
ad4a8aa76d | ||
|
|
c67dcebbbe | ||
|
|
0e135adbe2 | ||
|
|
43e83e7cee | ||
|
|
d1dcc6ced5 | ||
|
|
8425f195f8 | ||
|
|
055177f726 | ||
|
|
1674df2c0f | ||
|
|
29fa117d36 | ||
|
|
f08eaae7ed | ||
|
|
9573bb6af7 | ||
|
|
cb6bafcac6 | ||
|
|
d89aae5371 | ||
|
|
0b0ffe06d4 | ||
|
|
2ab720ff87 | ||
|
|
b9b07790d7 | ||
|
|
3dca34952b | ||
|
|
5be124b54a | ||
|
|
51dd0d3fdc | ||
|
|
7955f15298 | ||
|
|
fdf6091006 | ||
|
|
bb1c5792cc | ||
|
|
75544c943c | ||
|
|
db9967d4fd | ||
|
|
07a66ca62c | ||
|
|
3e3dc4392c | ||
|
|
57504a1795 | ||
|
|
9e9d2699b5 | ||
|
|
4a0d9220ba | ||
|
|
86a7771d5b | ||
|
|
d792a6b3a9 | ||
|
|
e8a083b7bd | ||
|
|
41ed6ab364 | ||
|
|
b587e9ad37 | ||
|
|
e3f9fafcdf | ||
|
|
c0ba743d70 | ||
|
|
523660006d | ||
|
|
aef9429f21 | ||
|
|
58e2bba1ed | ||
|
|
cee44bcdc3 | ||
|
|
9a120bc0d5 | ||
|
|
d1415a318c | ||
|
|
d701b9b1bd | ||
|
|
f8fe143809 | ||
|
|
e626db3b8c | ||
|
|
9e0491ef2f | ||
|
|
053bd91984 | ||
|
|
c76059cf7f | ||
|
|
6d6bc78d53 | ||
|
|
8855495822 | ||
|
|
3491fea313 | ||
|
|
66f5e2fbc3 | ||
|
|
3640bf8ae7 | ||
|
|
977edf6e5d | ||
|
|
e8f067a0b2 | ||
|
|
f971e2aa4c | ||
|
|
b15b92d161 | ||
|
|
1c5f66b5a9 | ||
|
|
1f77357c7d | ||
|
|
aaeb5db3c7 | ||
|
|
996a0894ae | ||
|
|
66fa3fc808 | ||
|
|
dab55daf95 | ||
|
|
7f1c31e03f | ||
|
|
0a8255f091 | ||
|
|
9f3e6650a9 | ||
|
|
4a17da3df5 | ||
|
|
2c4f0d4d8c | ||
|
|
9c1c2fb0d4 | ||
|
|
2332e4bf62 | ||
|
|
a488ac1b27 | ||
|
|
6daa41ca66 | ||
|
|
cc9517f1a2 | ||
|
|
c53d18e068 | ||
|
|
200a471e55 | ||
|
|
c21d37bd91 | ||
|
|
e36cd0e60b | ||
|
|
871f55bf11 | ||
|
|
22c9fed663 | ||
|
|
ea362d7a82 | ||
|
|
9ae9347f89 | ||
|
|
ae8bb902f9 | ||
|
|
90eeec23de | ||
|
|
fe8ad1fa74 | ||
|
|
dfc0a96567 | ||
|
|
474fd094c4 | ||
|
|
937d8fa4f7 | ||
|
|
45c9844616 | ||
|
|
12b8ef5a54 | ||
|
|
18f72c224e | ||
|
|
7ca3aaa83f | ||
|
|
04b1443e5a | ||
|
|
c461741778 | ||
|
|
2865b0a803 | ||
|
|
21e49be22f | ||
|
|
fef761cbab | ||
|
|
c15a353dc2 | ||
|
|
ffb32766c1 | ||
|
|
038908550e | ||
|
|
42f59134ae | ||
|
|
fc0014c0b5 | ||
|
|
42d8df3036 | ||
|
|
1fad9ca1cc | ||
|
|
ae289be77a | ||
|
|
7f6bfe9c6e | ||
|
|
ead4001b7a | ||
|
|
7b95ef72a0 | ||
|
|
a4556bf598 | ||
|
|
8d6268dc92 | ||
|
|
7ffcbdf60a | ||
|
|
76989ddc45 | ||
|
|
1db1254617 | ||
|
|
9810bffddc | ||
|
|
b25e18107b | ||
|
|
edc5fe5d1b | ||
|
|
7ffb44b3a4 | ||
|
|
32f4c33140 | ||
|
|
1a7b09c91c | ||
|
|
e5bf8e0e58 | ||
|
|
94725c533c | ||
|
|
359c92b64f | ||
|
|
8f8b8ad943 | ||
|
|
dd2f329fd5 | ||
|
|
813f594cb4 | ||
|
|
0e0ce49867 | ||
|
|
e485d318b7 | ||
|
|
4e82d81df1 | ||
|
|
d5dbda201b | ||
|
|
831258506b | ||
|
|
67f3329ecb | ||
|
|
ed7e6751f0 | ||
|
|
35e69486d3 | ||
|
|
918c8830e0 | ||
|
|
c3b4a4b955 | ||
|
|
44a14fabbd | ||
|
|
49399cd1fa | ||
|
|
2eb70be937 | ||
|
|
fc4cd2e942 | ||
|
|
cd6e457dc5 | ||
|
|
3ef138c9fe | ||
|
|
2e9bf3a4e5 | ||
|
|
547ceea4b0 | ||
|
|
776ff5e7ea | ||
|
|
2b3bac0d43 | ||
|
|
4e21643bbe | ||
|
|
e48efe2e8d | ||
|
|
5f6382fbc0 | ||
|
|
3d5d82081a | ||
|
|
cff96b1306 | ||
|
|
98c5a9c096 | ||
|
|
e92430b3ed | ||
|
|
848d1bfe64 | ||
|
|
a386283530 | ||
|
|
6101031269 | ||
|
|
2fc3431f46 | ||
|
|
361fa2c768 | ||
|
|
d9d9946faf | ||
|
|
f4a0a2466b | ||
|
|
dbf225d6ad | ||
|
|
4773a3831c | ||
|
|
6a19690581 | ||
|
|
b7a771d58d | ||
|
|
e3daefb81a | ||
|
|
b4253dace8 | ||
|
|
fcf3be1be1 | ||
|
|
99aebbad81 | ||
|
|
81b695a2a9 | ||
|
|
2dbba27357 | ||
|
|
032dfa949d | ||
|
|
7e703ed405 | ||
|
|
3b0cc08e6b | ||
|
|
8961a4a10d | ||
|
|
fed580ae18 | ||
|
|
97fa85a3f7 | ||
|
|
defe36bba1 | ||
|
|
f036869f53 | ||
|
|
3a1b36d594 | ||
|
|
b9ba747327 | ||
|
|
5631e1d57b | ||
|
|
740a5628dd | ||
|
|
0a758561f3 | ||
|
|
4986b1f084 | ||
|
|
7aaad4e7f3 | ||
|
|
b0497bfa07 | ||
|
|
711d214741 | ||
|
|
0795c67354 | ||
|
|
2d0f02cb8a | ||
|
|
e9a9f68568 | ||
|
|
1ae72235fc | ||
|
|
86f2a3a7d0 | ||
|
|
5b106d4827 | ||
|
|
3bf2eb0399 | ||
|
|
8302afda19 | ||
|
|
ba970ac7a5 | ||
|
|
89018e497f | ||
|
|
53a05eb781 | ||
|
|
7637915bed | ||
|
|
d5dd55a813 | ||
|
|
e80a0c39f8 | ||
|
|
357199658f |
@@ -6,6 +6,7 @@ _releases/
|
|||||||
*.min.js
|
*.min.js
|
||||||
**/commands/index.ts
|
**/commands/index.ts
|
||||||
**/node_modules/
|
**/node_modules/
|
||||||
|
**/abcjs-basic-min.js
|
||||||
packages/generator-joplin/generators/app/templates/api/
|
packages/generator-joplin/generators/app/templates/api/
|
||||||
Assets/
|
Assets/
|
||||||
docs/
|
docs/
|
||||||
@@ -96,7 +97,7 @@ packages/onenote-converter/renderer/pkg/*
|
|||||||
packages/app-cli/app/LinkSelector.js
|
packages/app-cli/app/LinkSelector.js
|
||||||
packages/app-cli/app/app.js
|
packages/app-cli/app/app.js
|
||||||
packages/app-cli/app/base-command.js
|
packages/app-cli/app/base-command.js
|
||||||
packages/app-cli/app/cli-integration-tests.js
|
packages/app-cli/app/cli-integration-tests.test.js
|
||||||
packages/app-cli/app/command-apidoc.js
|
packages/app-cli/app/command-apidoc.js
|
||||||
packages/app-cli/app/command-attach.js
|
packages/app-cli/app/command-attach.js
|
||||||
packages/app-cli/app/command-batch.js
|
packages/app-cli/app/command-batch.js
|
||||||
@@ -164,8 +165,6 @@ packages/app-desktop/app.reducer.js
|
|||||||
packages/app-desktop/app.js
|
packages/app-desktop/app.js
|
||||||
packages/app-desktop/bridge.js
|
packages/app-desktop/bridge.js
|
||||||
packages/app-desktop/checkForUpdates.js
|
packages/app-desktop/checkForUpdates.js
|
||||||
packages/app-desktop/commands/convertNoteToMarkdown.test.js
|
|
||||||
packages/app-desktop/commands/convertNoteToMarkdown.js
|
|
||||||
packages/app-desktop/commands/copyDevCommand.js
|
packages/app-desktop/commands/copyDevCommand.js
|
||||||
packages/app-desktop/commands/copyToClipboard.js
|
packages/app-desktop/commands/copyToClipboard.js
|
||||||
packages/app-desktop/commands/editProfileConfig.js
|
packages/app-desktop/commands/editProfileConfig.js
|
||||||
@@ -182,6 +181,7 @@ packages/app-desktop/commands/openProfileDirectory.js
|
|||||||
packages/app-desktop/commands/openSecondaryAppInstance.js
|
packages/app-desktop/commands/openSecondaryAppInstance.js
|
||||||
packages/app-desktop/commands/replaceMisspelling.js
|
packages/app-desktop/commands/replaceMisspelling.js
|
||||||
packages/app-desktop/commands/restoreNoteRevision.js
|
packages/app-desktop/commands/restoreNoteRevision.js
|
||||||
|
packages/app-desktop/commands/showProfileEditor.js
|
||||||
packages/app-desktop/commands/startExternalEditing.js
|
packages/app-desktop/commands/startExternalEditing.js
|
||||||
packages/app-desktop/commands/stopExternalEditing.js
|
packages/app-desktop/commands/stopExternalEditing.js
|
||||||
packages/app-desktop/commands/switchProfile.js
|
packages/app-desktop/commands/switchProfile.js
|
||||||
@@ -206,7 +206,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
|||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||||
packages/app-desktop/gui/ConversionNotification/ConversionNotification.js
|
|
||||||
packages/app-desktop/gui/Dialog.js
|
packages/app-desktop/gui/Dialog.js
|
||||||
packages/app-desktop/gui/DialogButtonRow.js
|
packages/app-desktop/gui/DialogButtonRow.js
|
||||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||||
@@ -392,6 +391,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
|||||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||||
packages/app-desktop/gui/PopupNotification/types.js
|
packages/app-desktop/gui/PopupNotification/types.js
|
||||||
|
packages/app-desktop/gui/ProfileEditor.js
|
||||||
packages/app-desktop/gui/PromptDialog.js
|
packages/app-desktop/gui/PromptDialog.js
|
||||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||||
@@ -424,10 +424,11 @@ packages/app-desktop/gui/Sidebar/Sidebar.js
|
|||||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
|
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
|
||||||
packages/app-desktop/gui/Sidebar/commands/index.js
|
packages/app-desktop/gui/Sidebar/commands/index.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js
|
packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js
|
||||||
|
packages/app-desktop/gui/Sidebar/hooks/useOnItemClick.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js
|
packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js
|
packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js
|
packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndex.js
|
packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndexes.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js
|
packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js
|
packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js
|
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js
|
||||||
@@ -468,6 +469,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||||
@@ -510,6 +512,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||||
@@ -561,6 +564,7 @@ packages/app-desktop/integration-tests/util/evaluateWithRetry.js
|
|||||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||||
packages/app-desktop/integration-tests/util/getMainWindow.js
|
packages/app-desktop/integration-tests/util/getMainWindow.js
|
||||||
|
packages/app-desktop/integration-tests/util/mockClipboard.js
|
||||||
packages/app-desktop/integration-tests/util/retryOnFailure.js
|
packages/app-desktop/integration-tests/util/retryOnFailure.js
|
||||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||||
@@ -701,6 +705,7 @@ packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
|||||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||||
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
||||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||||
|
packages/app-mobile/components/NoteEditor/MarkdownEditor.test.js
|
||||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||||
@@ -848,6 +853,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
|||||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||||
packages/app-mobile/components/screens/Notes/Notes.js
|
packages/app-mobile/components/screens/Notes/Notes.js
|
||||||
|
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||||
@@ -1004,6 +1010,7 @@ packages/editor/CodeMirror/CodeMirrorControl.js
|
|||||||
packages/editor/CodeMirror/configFromSettings.js
|
packages/editor/CodeMirror/configFromSettings.js
|
||||||
packages/editor/CodeMirror/createEditor.test.js
|
packages/editor/CodeMirror/createEditor.test.js
|
||||||
packages/editor/CodeMirror/createEditor.js
|
packages/editor/CodeMirror/createEditor.js
|
||||||
|
packages/editor/CodeMirror/editorCommands/cutOrCopyText.js
|
||||||
packages/editor/CodeMirror/editorCommands/duplicateLine.test.js
|
packages/editor/CodeMirror/editorCommands/duplicateLine.test.js
|
||||||
packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
||||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||||
@@ -1023,6 +1030,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
|||||||
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
||||||
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
|
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
|
||||||
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
|
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
|
||||||
|
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
|
||||||
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
|
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
|
||||||
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
||||||
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
||||||
@@ -1046,6 +1054,7 @@ packages/editor/CodeMirror/extensions/rendering/addFormattingClasses.js
|
|||||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.test.js
|
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.test.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.js
|
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/renderingExtension.js
|
packages/editor/CodeMirror/extensions/rendering/renderingExtension.js
|
||||||
|
packages/editor/CodeMirror/extensions/rendering/replaceBackslashEscapes.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||||
@@ -1112,10 +1121,12 @@ packages/editor/ProseMirror/plugins/detailsPlugin.js
|
|||||||
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
||||||
packages/editor/ProseMirror/plugins/imagePlugin.js
|
packages/editor/ProseMirror/plugins/imagePlugin.js
|
||||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/postProcessRenderedHtml.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.test.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/utils/createEditorDialog.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/utils/postProcessRenderedHtml.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||||
@@ -1130,6 +1141,7 @@ packages/editor/ProseMirror/schema.js
|
|||||||
packages/editor/ProseMirror/styles.js
|
packages/editor/ProseMirror/styles.js
|
||||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||||
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
||||||
|
packages/editor/ProseMirror/testing/mockEditorApi.js
|
||||||
packages/editor/ProseMirror/types.js
|
packages/editor/ProseMirror/types.js
|
||||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||||
@@ -1143,6 +1155,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
|
|||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||||
|
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
|
||||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||||
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
|
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
|
||||||
packages/editor/ProseMirror/utils/postprocessEditorOutput.test.js
|
packages/editor/ProseMirror/utils/postprocessEditorOutput.test.js
|
||||||
@@ -1232,6 +1245,8 @@ packages/lib/callbackUrlUtils.js
|
|||||||
packages/lib/clipperUtils.js
|
packages/lib/clipperUtils.js
|
||||||
packages/lib/commands/convertHtmlToMarkdown.test.js
|
packages/lib/commands/convertHtmlToMarkdown.test.js
|
||||||
packages/lib/commands/convertHtmlToMarkdown.js
|
packages/lib/commands/convertHtmlToMarkdown.js
|
||||||
|
packages/lib/commands/convertNoteToMarkdown.test.js
|
||||||
|
packages/lib/commands/convertNoteToMarkdown.js
|
||||||
packages/lib/commands/deleteNote.js
|
packages/lib/commands/deleteNote.js
|
||||||
packages/lib/commands/historyBackward.js
|
packages/lib/commands/historyBackward.js
|
||||||
packages/lib/commands/historyForward.js
|
packages/lib/commands/historyForward.js
|
||||||
@@ -1360,6 +1375,7 @@ packages/lib/models/utils/getCanBeCollapsedFolderIds.js
|
|||||||
packages/lib/models/utils/getCollator.js
|
packages/lib/models/utils/getCollator.js
|
||||||
packages/lib/models/utils/getConflictFolderId.js
|
packages/lib/models/utils/getConflictFolderId.js
|
||||||
packages/lib/models/utils/isItemId.js
|
packages/lib/models/utils/isItemId.js
|
||||||
|
packages/lib/models/utils/isJoplinServerVariant.js
|
||||||
packages/lib/models/utils/itemCanBeEncrypted.js
|
packages/lib/models/utils/itemCanBeEncrypted.js
|
||||||
packages/lib/models/utils/onFolderDrop.test.js
|
packages/lib/models/utils/onFolderDrop.test.js
|
||||||
packages/lib/models/utils/onFolderDrop.js
|
packages/lib/models/utils/onFolderDrop.js
|
||||||
@@ -1395,6 +1411,7 @@ packages/lib/services/KeymapService_keysRegExp.js
|
|||||||
packages/lib/services/KvStore.js
|
packages/lib/services/KvStore.js
|
||||||
packages/lib/services/MigrationService.js
|
packages/lib/services/MigrationService.js
|
||||||
packages/lib/services/NavService.js
|
packages/lib/services/NavService.js
|
||||||
|
packages/lib/services/NotePositionService.js
|
||||||
packages/lib/services/PostMessageService.js
|
packages/lib/services/PostMessageService.js
|
||||||
packages/lib/services/ReportService.test.js
|
packages/lib/services/ReportService.test.js
|
||||||
packages/lib/services/ReportService.js
|
packages/lib/services/ReportService.js
|
||||||
@@ -1410,6 +1427,7 @@ packages/lib/services/UndoRedoService.js
|
|||||||
packages/lib/services/WhenClause.test.js
|
packages/lib/services/WhenClause.test.js
|
||||||
packages/lib/services/WhenClause.js
|
packages/lib/services/WhenClause.js
|
||||||
packages/lib/services/commands/MenuUtils.js
|
packages/lib/services/commands/MenuUtils.js
|
||||||
|
packages/lib/services/commands/ToolbarButtonUtils.test.js
|
||||||
packages/lib/services/commands/ToolbarButtonUtils.js
|
packages/lib/services/commands/ToolbarButtonUtils.js
|
||||||
packages/lib/services/commands/commandsToMarkdownTable.js
|
packages/lib/services/commands/commandsToMarkdownTable.js
|
||||||
packages/lib/services/commands/focusEditorIfEditorCommand.js
|
packages/lib/services/commands/focusEditorIfEditorCommand.js
|
||||||
@@ -1643,6 +1661,7 @@ packages/lib/services/synchronizer/Synchronizer.sharing.test.js
|
|||||||
packages/lib/services/synchronizer/Synchronizer.tags.test.js
|
packages/lib/services/synchronizer/Synchronizer.tags.test.js
|
||||||
packages/lib/services/synchronizer/Synchronizer.tools.test.js
|
packages/lib/services/synchronizer/Synchronizer.tools.test.js
|
||||||
packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.js
|
packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.js
|
||||||
|
packages/lib/services/synchronizer/handleConflictAction.test.js
|
||||||
packages/lib/services/synchronizer/migrations/1.js
|
packages/lib/services/synchronizer/migrations/1.js
|
||||||
packages/lib/services/synchronizer/migrations/2.js
|
packages/lib/services/synchronizer/migrations/2.js
|
||||||
packages/lib/services/synchronizer/migrations/3.js
|
packages/lib/services/synchronizer/migrations/3.js
|
||||||
@@ -1777,6 +1796,7 @@ packages/renderer/MdToHtml/createEventHandlingAttrs.js
|
|||||||
packages/renderer/MdToHtml/linkReplacement.test.js
|
packages/renderer/MdToHtml/linkReplacement.test.js
|
||||||
packages/renderer/MdToHtml/linkReplacement.js
|
packages/renderer/MdToHtml/linkReplacement.js
|
||||||
packages/renderer/MdToHtml/renderMedia.js
|
packages/renderer/MdToHtml/renderMedia.js
|
||||||
|
packages/renderer/MdToHtml/rules/abc.js
|
||||||
packages/renderer/MdToHtml/rules/checkbox.js
|
packages/renderer/MdToHtml/rules/checkbox.js
|
||||||
packages/renderer/MdToHtml/rules/code_inline.js
|
packages/renderer/MdToHtml/rules/code_inline.js
|
||||||
packages/renderer/MdToHtml/rules/fence.js
|
packages/renderer/MdToHtml/rules/fence.js
|
||||||
@@ -1819,19 +1839,24 @@ packages/tools/fuzzer/Client.js
|
|||||||
packages/tools/fuzzer/ClientPool.js
|
packages/tools/fuzzer/ClientPool.js
|
||||||
packages/tools/fuzzer/Server.js
|
packages/tools/fuzzer/Server.js
|
||||||
packages/tools/fuzzer/constants.js
|
packages/tools/fuzzer/constants.js
|
||||||
|
packages/tools/fuzzer/doRandomAction.js
|
||||||
packages/tools/fuzzer/model/FolderRecord.js
|
packages/tools/fuzzer/model/FolderRecord.js
|
||||||
packages/tools/fuzzer/sync-fuzzer.js
|
packages/tools/fuzzer/sync-fuzzer.js
|
||||||
packages/tools/fuzzer/types.js
|
packages/tools/fuzzer/types.js
|
||||||
|
packages/tools/fuzzer/utils/ProgressBar.js
|
||||||
packages/tools/fuzzer/utils/SeededRandom.js
|
packages/tools/fuzzer/utils/SeededRandom.js
|
||||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||||
packages/tools/fuzzer/utils/getProperty.js
|
packages/tools/fuzzer/utils/getProperty.js
|
||||||
packages/tools/fuzzer/utils/getStringProperty.js
|
packages/tools/fuzzer/utils/getStringProperty.js
|
||||||
|
packages/tools/fuzzer/utils/logDiffDebug.js
|
||||||
packages/tools/fuzzer/utils/openDebugSession.js
|
packages/tools/fuzzer/utils/openDebugSession.js
|
||||||
|
packages/tools/fuzzer/utils/randomString.js
|
||||||
packages/tools/fuzzer/utils/retryWithCount.js
|
packages/tools/fuzzer/utils/retryWithCount.js
|
||||||
packages/tools/generate-database-types.js
|
packages/tools/generate-database-types.js
|
||||||
packages/tools/generate-images.js
|
packages/tools/generate-images.js
|
||||||
packages/tools/git-changelog.test.js
|
packages/tools/git-changelog.test.js
|
||||||
packages/tools/git-changelog.js
|
packages/tools/git-changelog.js
|
||||||
|
packages/tools/licenses/buildReport.js
|
||||||
packages/tools/licenses/getLicenses.js
|
packages/tools/licenses/getLicenses.js
|
||||||
packages/tools/licenses/licenseChecker.js
|
packages/tools/licenses/licenseChecker.js
|
||||||
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
|
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
|
||||||
|
|||||||
13
.github/workflows/build-android.yml
vendored
@@ -21,19 +21,24 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '24'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install Yarn
|
- name: Install Yarn
|
||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
env:
|
||||||
|
SKIP_ONENOTE_CONVERTER_BUILD: 1
|
||||||
|
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet || true
|
||||||
|
sudo rm -rf /opt/ghc || true
|
||||||
|
|
||||||
- name: Assemble Android Release
|
- name: Assemble Android Release
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
6
.github/workflows/build-macos-m1.yml
vendored
@@ -9,11 +9,9 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: olegtarasov/get-tag@v2.1.4
|
- uses: olegtarasov/get-tag@v2.1.4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
node-version: '24'
|
||||||
# https://github.com/facebook/react-native/issues/36440
|
|
||||||
node-version: '18.20.8'
|
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Install Yarn
|
- name: Install Yarn
|
||||||
|
|||||||
4
.github/workflows/github-actions-main.yml
vendored
@@ -147,9 +147,9 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '24'
|
||||||
|
|
||||||
- name: Free disk space
|
- name: Free disk space
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ runs:
|
|||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
if: ${{ runner.os != 'Windows' }}
|
if: ${{ runner.os != 'Windows' }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '18.20.8'
|
node-version: '24'
|
||||||
# Disable the cache on ARM runners. For now, we don't run "yarn install" on these
|
# Disable the cache on ARM runners. For now, we don't run "yarn install" on these
|
||||||
# environments and this breaks actions/setup-node.
|
# environments and this breaks actions/setup-node.
|
||||||
# See https://github.com/laurent22/joplin/commit/47d0d3eb9e89153a609fb5441344da10904c6308#commitcomment-159577783.
|
# See https://github.com/laurent22/joplin/commit/47d0d3eb9e89153a609fb5441344da10904c6308#commitcomment-159577783.
|
||||||
|
|||||||
38
.gitignore
vendored
@@ -69,7 +69,7 @@ docs/**/*.mustache
|
|||||||
packages/app-cli/app/LinkSelector.js
|
packages/app-cli/app/LinkSelector.js
|
||||||
packages/app-cli/app/app.js
|
packages/app-cli/app/app.js
|
||||||
packages/app-cli/app/base-command.js
|
packages/app-cli/app/base-command.js
|
||||||
packages/app-cli/app/cli-integration-tests.js
|
packages/app-cli/app/cli-integration-tests.test.js
|
||||||
packages/app-cli/app/command-apidoc.js
|
packages/app-cli/app/command-apidoc.js
|
||||||
packages/app-cli/app/command-attach.js
|
packages/app-cli/app/command-attach.js
|
||||||
packages/app-cli/app/command-batch.js
|
packages/app-cli/app/command-batch.js
|
||||||
@@ -137,8 +137,6 @@ packages/app-desktop/app.reducer.js
|
|||||||
packages/app-desktop/app.js
|
packages/app-desktop/app.js
|
||||||
packages/app-desktop/bridge.js
|
packages/app-desktop/bridge.js
|
||||||
packages/app-desktop/checkForUpdates.js
|
packages/app-desktop/checkForUpdates.js
|
||||||
packages/app-desktop/commands/convertNoteToMarkdown.test.js
|
|
||||||
packages/app-desktop/commands/convertNoteToMarkdown.js
|
|
||||||
packages/app-desktop/commands/copyDevCommand.js
|
packages/app-desktop/commands/copyDevCommand.js
|
||||||
packages/app-desktop/commands/copyToClipboard.js
|
packages/app-desktop/commands/copyToClipboard.js
|
||||||
packages/app-desktop/commands/editProfileConfig.js
|
packages/app-desktop/commands/editProfileConfig.js
|
||||||
@@ -155,6 +153,7 @@ packages/app-desktop/commands/openProfileDirectory.js
|
|||||||
packages/app-desktop/commands/openSecondaryAppInstance.js
|
packages/app-desktop/commands/openSecondaryAppInstance.js
|
||||||
packages/app-desktop/commands/replaceMisspelling.js
|
packages/app-desktop/commands/replaceMisspelling.js
|
||||||
packages/app-desktop/commands/restoreNoteRevision.js
|
packages/app-desktop/commands/restoreNoteRevision.js
|
||||||
|
packages/app-desktop/commands/showProfileEditor.js
|
||||||
packages/app-desktop/commands/startExternalEditing.js
|
packages/app-desktop/commands/startExternalEditing.js
|
||||||
packages/app-desktop/commands/stopExternalEditing.js
|
packages/app-desktop/commands/stopExternalEditing.js
|
||||||
packages/app-desktop/commands/switchProfile.js
|
packages/app-desktop/commands/switchProfile.js
|
||||||
@@ -179,7 +178,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
|||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||||
packages/app-desktop/gui/ConversionNotification/ConversionNotification.js
|
|
||||||
packages/app-desktop/gui/Dialog.js
|
packages/app-desktop/gui/Dialog.js
|
||||||
packages/app-desktop/gui/DialogButtonRow.js
|
packages/app-desktop/gui/DialogButtonRow.js
|
||||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||||
@@ -365,6 +363,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
|
|||||||
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||||
packages/app-desktop/gui/PopupNotification/types.js
|
packages/app-desktop/gui/PopupNotification/types.js
|
||||||
|
packages/app-desktop/gui/ProfileEditor.js
|
||||||
packages/app-desktop/gui/PromptDialog.js
|
packages/app-desktop/gui/PromptDialog.js
|
||||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||||
@@ -397,10 +396,11 @@ packages/app-desktop/gui/Sidebar/Sidebar.js
|
|||||||
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
|
packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.js
|
||||||
packages/app-desktop/gui/Sidebar/commands/index.js
|
packages/app-desktop/gui/Sidebar/commands/index.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js
|
packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.js
|
||||||
|
packages/app-desktop/gui/Sidebar/hooks/useOnItemClick.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js
|
packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js
|
packages/app-desktop/gui/Sidebar/hooks/useOnRenderListWrapper.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js
|
packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndex.js
|
packages/app-desktop/gui/Sidebar/hooks/useSelectedSidebarIndexes.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js
|
packages/app-desktop/gui/Sidebar/hooks/useSidebarCommandHandler.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js
|
packages/app-desktop/gui/Sidebar/hooks/useSidebarListData.js
|
||||||
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js
|
packages/app-desktop/gui/Sidebar/hooks/utils/toggleHeader.js
|
||||||
@@ -441,6 +441,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/importFrom.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||||
@@ -483,6 +484,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/types.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js
|
||||||
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/showFolderPicker.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||||
@@ -534,6 +536,7 @@ packages/app-desktop/integration-tests/util/evaluateWithRetry.js
|
|||||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||||
packages/app-desktop/integration-tests/util/getMainWindow.js
|
packages/app-desktop/integration-tests/util/getMainWindow.js
|
||||||
|
packages/app-desktop/integration-tests/util/mockClipboard.js
|
||||||
packages/app-desktop/integration-tests/util/retryOnFailure.js
|
packages/app-desktop/integration-tests/util/retryOnFailure.js
|
||||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||||
@@ -674,6 +677,7 @@ packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
|||||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||||
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
|
||||||
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
|
||||||
|
packages/app-mobile/components/NoteEditor/MarkdownEditor.test.js
|
||||||
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
packages/app-mobile/components/NoteEditor/MarkdownEditor.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||||
@@ -821,6 +825,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
|||||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||||
packages/app-mobile/components/screens/Notes/Notes.js
|
packages/app-mobile/components/screens/Notes/Notes.js
|
||||||
|
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||||
@@ -977,6 +982,7 @@ packages/editor/CodeMirror/CodeMirrorControl.js
|
|||||||
packages/editor/CodeMirror/configFromSettings.js
|
packages/editor/CodeMirror/configFromSettings.js
|
||||||
packages/editor/CodeMirror/createEditor.test.js
|
packages/editor/CodeMirror/createEditor.test.js
|
||||||
packages/editor/CodeMirror/createEditor.js
|
packages/editor/CodeMirror/createEditor.js
|
||||||
|
packages/editor/CodeMirror/editorCommands/cutOrCopyText.js
|
||||||
packages/editor/CodeMirror/editorCommands/duplicateLine.test.js
|
packages/editor/CodeMirror/editorCommands/duplicateLine.test.js
|
||||||
packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
packages/editor/CodeMirror/editorCommands/duplicateLine.js
|
||||||
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||||
@@ -996,6 +1002,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
|||||||
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
|
||||||
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
|
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
|
||||||
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
|
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
|
||||||
|
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
|
||||||
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
|
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
|
||||||
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
|
||||||
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
|
||||||
@@ -1019,6 +1026,7 @@ packages/editor/CodeMirror/extensions/rendering/addFormattingClasses.js
|
|||||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.test.js
|
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.test.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.js
|
packages/editor/CodeMirror/extensions/rendering/renderBlockImages.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/renderingExtension.js
|
packages/editor/CodeMirror/extensions/rendering/renderingExtension.js
|
||||||
|
packages/editor/CodeMirror/extensions/rendering/replaceBackslashEscapes.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||||
@@ -1085,10 +1093,12 @@ packages/editor/ProseMirror/plugins/detailsPlugin.js
|
|||||||
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
packages/editor/ProseMirror/plugins/imagePlugin.test.js
|
||||||
packages/editor/ProseMirror/plugins/imagePlugin.js
|
packages/editor/ProseMirror/plugins/imagePlugin.js
|
||||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin/postProcessRenderedHtml.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.test.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/utils/createEditorDialog.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/utils/postProcessRenderedHtml.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||||
@@ -1103,6 +1113,7 @@ packages/editor/ProseMirror/schema.js
|
|||||||
packages/editor/ProseMirror/styles.js
|
packages/editor/ProseMirror/styles.js
|
||||||
packages/editor/ProseMirror/testing/createTestEditor.js
|
packages/editor/ProseMirror/testing/createTestEditor.js
|
||||||
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
packages/editor/ProseMirror/testing/createTestEditorWithSerializer.js
|
||||||
|
packages/editor/ProseMirror/testing/mockEditorApi.js
|
||||||
packages/editor/ProseMirror/types.js
|
packages/editor/ProseMirror/types.js
|
||||||
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
packages/editor/ProseMirror/utils/SelectableNodeView.js
|
||||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||||
@@ -1116,6 +1127,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
|
|||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||||
packages/editor/ProseMirror/utils/forEachHeading.js
|
packages/editor/ProseMirror/utils/forEachHeading.js
|
||||||
|
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
|
||||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||||
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
|
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
|
||||||
packages/editor/ProseMirror/utils/postprocessEditorOutput.test.js
|
packages/editor/ProseMirror/utils/postprocessEditorOutput.test.js
|
||||||
@@ -1205,6 +1217,8 @@ packages/lib/callbackUrlUtils.js
|
|||||||
packages/lib/clipperUtils.js
|
packages/lib/clipperUtils.js
|
||||||
packages/lib/commands/convertHtmlToMarkdown.test.js
|
packages/lib/commands/convertHtmlToMarkdown.test.js
|
||||||
packages/lib/commands/convertHtmlToMarkdown.js
|
packages/lib/commands/convertHtmlToMarkdown.js
|
||||||
|
packages/lib/commands/convertNoteToMarkdown.test.js
|
||||||
|
packages/lib/commands/convertNoteToMarkdown.js
|
||||||
packages/lib/commands/deleteNote.js
|
packages/lib/commands/deleteNote.js
|
||||||
packages/lib/commands/historyBackward.js
|
packages/lib/commands/historyBackward.js
|
||||||
packages/lib/commands/historyForward.js
|
packages/lib/commands/historyForward.js
|
||||||
@@ -1333,6 +1347,7 @@ packages/lib/models/utils/getCanBeCollapsedFolderIds.js
|
|||||||
packages/lib/models/utils/getCollator.js
|
packages/lib/models/utils/getCollator.js
|
||||||
packages/lib/models/utils/getConflictFolderId.js
|
packages/lib/models/utils/getConflictFolderId.js
|
||||||
packages/lib/models/utils/isItemId.js
|
packages/lib/models/utils/isItemId.js
|
||||||
|
packages/lib/models/utils/isJoplinServerVariant.js
|
||||||
packages/lib/models/utils/itemCanBeEncrypted.js
|
packages/lib/models/utils/itemCanBeEncrypted.js
|
||||||
packages/lib/models/utils/onFolderDrop.test.js
|
packages/lib/models/utils/onFolderDrop.test.js
|
||||||
packages/lib/models/utils/onFolderDrop.js
|
packages/lib/models/utils/onFolderDrop.js
|
||||||
@@ -1368,6 +1383,7 @@ packages/lib/services/KeymapService_keysRegExp.js
|
|||||||
packages/lib/services/KvStore.js
|
packages/lib/services/KvStore.js
|
||||||
packages/lib/services/MigrationService.js
|
packages/lib/services/MigrationService.js
|
||||||
packages/lib/services/NavService.js
|
packages/lib/services/NavService.js
|
||||||
|
packages/lib/services/NotePositionService.js
|
||||||
packages/lib/services/PostMessageService.js
|
packages/lib/services/PostMessageService.js
|
||||||
packages/lib/services/ReportService.test.js
|
packages/lib/services/ReportService.test.js
|
||||||
packages/lib/services/ReportService.js
|
packages/lib/services/ReportService.js
|
||||||
@@ -1383,6 +1399,7 @@ packages/lib/services/UndoRedoService.js
|
|||||||
packages/lib/services/WhenClause.test.js
|
packages/lib/services/WhenClause.test.js
|
||||||
packages/lib/services/WhenClause.js
|
packages/lib/services/WhenClause.js
|
||||||
packages/lib/services/commands/MenuUtils.js
|
packages/lib/services/commands/MenuUtils.js
|
||||||
|
packages/lib/services/commands/ToolbarButtonUtils.test.js
|
||||||
packages/lib/services/commands/ToolbarButtonUtils.js
|
packages/lib/services/commands/ToolbarButtonUtils.js
|
||||||
packages/lib/services/commands/commandsToMarkdownTable.js
|
packages/lib/services/commands/commandsToMarkdownTable.js
|
||||||
packages/lib/services/commands/focusEditorIfEditorCommand.js
|
packages/lib/services/commands/focusEditorIfEditorCommand.js
|
||||||
@@ -1616,6 +1633,7 @@ packages/lib/services/synchronizer/Synchronizer.sharing.test.js
|
|||||||
packages/lib/services/synchronizer/Synchronizer.tags.test.js
|
packages/lib/services/synchronizer/Synchronizer.tags.test.js
|
||||||
packages/lib/services/synchronizer/Synchronizer.tools.test.js
|
packages/lib/services/synchronizer/Synchronizer.tools.test.js
|
||||||
packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.js
|
packages/lib/services/synchronizer/gui/useSyncTargetUpgrade.js
|
||||||
|
packages/lib/services/synchronizer/handleConflictAction.test.js
|
||||||
packages/lib/services/synchronizer/migrations/1.js
|
packages/lib/services/synchronizer/migrations/1.js
|
||||||
packages/lib/services/synchronizer/migrations/2.js
|
packages/lib/services/synchronizer/migrations/2.js
|
||||||
packages/lib/services/synchronizer/migrations/3.js
|
packages/lib/services/synchronizer/migrations/3.js
|
||||||
@@ -1750,6 +1768,7 @@ packages/renderer/MdToHtml/createEventHandlingAttrs.js
|
|||||||
packages/renderer/MdToHtml/linkReplacement.test.js
|
packages/renderer/MdToHtml/linkReplacement.test.js
|
||||||
packages/renderer/MdToHtml/linkReplacement.js
|
packages/renderer/MdToHtml/linkReplacement.js
|
||||||
packages/renderer/MdToHtml/renderMedia.js
|
packages/renderer/MdToHtml/renderMedia.js
|
||||||
|
packages/renderer/MdToHtml/rules/abc.js
|
||||||
packages/renderer/MdToHtml/rules/checkbox.js
|
packages/renderer/MdToHtml/rules/checkbox.js
|
||||||
packages/renderer/MdToHtml/rules/code_inline.js
|
packages/renderer/MdToHtml/rules/code_inline.js
|
||||||
packages/renderer/MdToHtml/rules/fence.js
|
packages/renderer/MdToHtml/rules/fence.js
|
||||||
@@ -1792,19 +1811,24 @@ packages/tools/fuzzer/Client.js
|
|||||||
packages/tools/fuzzer/ClientPool.js
|
packages/tools/fuzzer/ClientPool.js
|
||||||
packages/tools/fuzzer/Server.js
|
packages/tools/fuzzer/Server.js
|
||||||
packages/tools/fuzzer/constants.js
|
packages/tools/fuzzer/constants.js
|
||||||
|
packages/tools/fuzzer/doRandomAction.js
|
||||||
packages/tools/fuzzer/model/FolderRecord.js
|
packages/tools/fuzzer/model/FolderRecord.js
|
||||||
packages/tools/fuzzer/sync-fuzzer.js
|
packages/tools/fuzzer/sync-fuzzer.js
|
||||||
packages/tools/fuzzer/types.js
|
packages/tools/fuzzer/types.js
|
||||||
|
packages/tools/fuzzer/utils/ProgressBar.js
|
||||||
packages/tools/fuzzer/utils/SeededRandom.js
|
packages/tools/fuzzer/utils/SeededRandom.js
|
||||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||||
packages/tools/fuzzer/utils/getProperty.js
|
packages/tools/fuzzer/utils/getProperty.js
|
||||||
packages/tools/fuzzer/utils/getStringProperty.js
|
packages/tools/fuzzer/utils/getStringProperty.js
|
||||||
|
packages/tools/fuzzer/utils/logDiffDebug.js
|
||||||
packages/tools/fuzzer/utils/openDebugSession.js
|
packages/tools/fuzzer/utils/openDebugSession.js
|
||||||
|
packages/tools/fuzzer/utils/randomString.js
|
||||||
packages/tools/fuzzer/utils/retryWithCount.js
|
packages/tools/fuzzer/utils/retryWithCount.js
|
||||||
packages/tools/generate-database-types.js
|
packages/tools/generate-database-types.js
|
||||||
packages/tools/generate-images.js
|
packages/tools/generate-images.js
|
||||||
packages/tools/git-changelog.test.js
|
packages/tools/git-changelog.test.js
|
||||||
packages/tools/git-changelog.js
|
packages/tools/git-changelog.js
|
||||||
|
packages/tools/licenses/buildReport.js
|
||||||
packages/tools/licenses/getLicenses.js
|
packages/tools/licenses/getLicenses.js
|
||||||
packages/tools/licenses/licenseChecker.js
|
packages/tools/licenses/licenseChecker.js
|
||||||
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
|
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
|
||||||
|
|||||||
BIN
Assets/Forum/Christmas/ForumChristmasBackgroundDark.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
Assets/Forum/Christmas/ForumChristmasBackgroundLight.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
Assets/ImageSources/Android/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_foreground_drawable.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
Assets/ImageSources/Android/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
Assets/WebsiteAssets/images/md_plugins/abc/PeacherineRag.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
Assets/WebsiteAssets/images/md_plugins/abc/Tablature.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
@@ -2,7 +2,7 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
FROM node:18 AS builder
|
FROM node:24 AS builder
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
@@ -58,7 +58,7 @@ RUN --mount=type=cache,target=/build/.yarn/cache --mount=type=cache,target=/buil
|
|||||||
# from a smaller base image.
|
# from a smaller base image.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
FROM node:18-slim
|
FROM node:24-slim
|
||||||
|
|
||||||
ARG user=joplin
|
ARG user=joplin
|
||||||
RUN useradd --create-home --shell /bin/bash $user
|
RUN useradd --create-home --shell /bin/bash $user
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-bullseye
|
FROM node:24-bullseye
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y \
|
&& apt-get install -y \
|
||||||
|
|||||||
@@ -67,6 +67,45 @@ showHelp() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Accepts two versions in symver (a.b.c).
|
||||||
|
# Echos -1 if the first version is less than the second,
|
||||||
|
# 0 if they're equal,
|
||||||
|
# 1 if the first version is greater than second.
|
||||||
|
compareVersions() {
|
||||||
|
V_MAJOR1=$(echo "$1"|cut -d. -f1)
|
||||||
|
V_MAJOR2=$(echo "$2"|cut -d. -f1)
|
||||||
|
|
||||||
|
if [[ $V_MAJOR1 -lt $V_MAJOR2 ]] ; then
|
||||||
|
echo -1
|
||||||
|
return
|
||||||
|
elif [[ $V_MAJOR1 -gt $V_MAJOR2 ]] ; then
|
||||||
|
echo 1
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
V_MINOR1=$(echo "$1"|cut -d. -f2)
|
||||||
|
V_MINOR2=$(echo "$2"|cut -d. -f2)
|
||||||
|
|
||||||
|
if [[ $V_MINOR1 -lt $V_MINOR2 ]] ; then
|
||||||
|
echo -1
|
||||||
|
return
|
||||||
|
elif [[ $V_MINOR1 -gt $V_MINOR2 ]] ; then
|
||||||
|
echo 1
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
V_PATCH1=$(echo "$1"|cut -d. -f3)
|
||||||
|
V_PATCH2=$(echo "$2"|cut -d. -f3)
|
||||||
|
|
||||||
|
if [[ $V_PATCH1 -lt $V_PATCH2 ]] ; then
|
||||||
|
echo -1
|
||||||
|
elif [[ $V_PATCH1 -gt $V_PATCH2 ]] ; then
|
||||||
|
echo 1
|
||||||
|
else
|
||||||
|
echo 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
#-----------------------------------------------------
|
#-----------------------------------------------------
|
||||||
# Setup Download Helper: DL
|
# Setup Download Helper: DL
|
||||||
#-----------------------------------------------------
|
#-----------------------------------------------------
|
||||||
@@ -159,12 +198,21 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if it's in the latest version
|
# Check if it's in the latest version
|
||||||
if [[ -e "${INSTALL_DIR}/VERSION" ]] && [[ $(< "${INSTALL_DIR}/VERSION") == "${RELEASE_VERSION}" ]]; then
|
if [[ -e "${INSTALL_DIR}/VERSION" ]]; then
|
||||||
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
|
||||||
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
|
VERSION_COMPARISON=$(compareVersions "$CURRENT_VERSION" "$RELEASE_VERSION")
|
||||||
|
|
||||||
|
if [[ "$VERSION_COMPARISON" == "0" ]]; then
|
||||||
|
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
|
||||||
|
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
|
||||||
|
elif [[ "$VERSION_COMPARISON" == "1" ]]; then
|
||||||
|
print "${COLOR_YELLOW}You have version ${CURRENT_VERSION} installed, which is newer than the latest published version ${RELEASE_VERSION}.${COLOR_RESET}"
|
||||||
|
print "${COLOR_YELLOW}Skipping installation to avoid downgrade.${COLOR_RESET}"
|
||||||
|
else
|
||||||
|
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION} installed."
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
[[ -e "${INSTALL_DIR}/VERSION" ]] && CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
|
print "The latest version is ${RELEASE_VERSION}, but you have no version installed."
|
||||||
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if it's an update or a new install
|
# Check if it's an update or a new install
|
||||||
@@ -236,7 +284,7 @@ if command -v lsb_release &> /dev/null; then
|
|||||||
# without writing the AppImage to a non-user-writable location (without invalidating other security
|
# without writing the AppImage to a non-user-writable location (without invalidating other security
|
||||||
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
|
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
|
||||||
HAS_USERNS_RESTRICTIONS=false
|
HAS_USERNS_RESTRICTIONS=false
|
||||||
if [[ "$DISTVER" =~ ^Ubuntu && $DISTMAJOR -ge 23 ]]; then
|
if [[ "$DISTVER" =~ ^(Ubuntu|Tuxedo) && $DISTMAJOR -ge 23 ]]; then
|
||||||
HAS_USERNS_RESTRICTIONS=true
|
HAS_USERNS_RESTRICTIONS=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -258,6 +306,15 @@ fi
|
|||||||
if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cinnamon.*|.*deepin.*|.*pantheon.*|.*lxde.*|.*i3.*|.*sway.* ]] || [[ `command -v update-desktop-database` ]]; then
|
if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cinnamon.*|.*deepin.*|.*pantheon.*|.*lxde.*|.*i3.*|.*sway.* ]] || [[ `command -v update-desktop-database` ]]; then
|
||||||
DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
|
DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
|
||||||
DESKTOP_FILE_LOCATION="$DATA_HOME/applications"
|
DESKTOP_FILE_LOCATION="$DATA_HOME/applications"
|
||||||
|
|
||||||
|
# Only later versions of Joplin default to Wayland
|
||||||
|
IS_WAYLAND_BY_DEFAULT=$(compareVersions "$RELEASE_VERSION" "3.5.6")
|
||||||
|
# Joplin has a different startup WM class on Wayland and X11:
|
||||||
|
STARTUP_WM_CLASS=Joplin
|
||||||
|
if [[ $XDG_SESSION_TYPE != "x11" && $IS_WAYLAND_BY_DEFAULT == "1" ]]; then
|
||||||
|
STARTUP_WM_CLASS=@joplin/app-desktop
|
||||||
|
fi
|
||||||
|
|
||||||
# Only delete the desktop file if it will be replaced
|
# Only delete the desktop file if it will be replaced
|
||||||
rm -f "$DESKTOP_FILE_LOCATION/appimagekit-joplin.desktop"
|
rm -f "$DESKTOP_FILE_LOCATION/appimagekit-joplin.desktop"
|
||||||
|
|
||||||
@@ -272,7 +329,9 @@ Name=Joplin
|
|||||||
Comment=Joplin for Desktop
|
Comment=Joplin for Desktop
|
||||||
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE "${INSTALL_DIR}/Joplin.AppImage" ${SANDBOXPARAM} %u
|
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE "${INSTALL_DIR}/Joplin.AppImage" ${SANDBOXPARAM} %u
|
||||||
Icon=joplin
|
Icon=joplin
|
||||||
StartupWMClass=Joplin
|
# This will be different between Wayland and X11. On Wayland, the startup
|
||||||
|
# WM class is "@joplin/app-desktop". On X11, it's "Joplin".
|
||||||
|
StartupWMClass=${STARTUP_WM_CLASS}
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Office;
|
Categories=Office;
|
||||||
MimeType=x-scheme-handler/joplin;
|
MimeType=x-scheme-handler/joplin;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- DONATELINKS -->
|
<!-- DONATELINKS -->
|
||||||
[](https://www.paypal.com/donate/?business=E8JMYD2LQ8MMA&no_recurring=0&item_name=I+rely+on+donations+to+maintain+and+improve+the+Joplin+open+source+project.+Thank+you+for+your+help+-+it+makes+a+difference%21¤cy_code=EUR) [](https://github.com/sponsors/laurent22/) [](https://www.patreon.com/joplin) [](https://joplinapp.org/donate/#donations)
|
[](https://www.paypal.com/donate/?hosted_button_id=WQCERTSSLCC7U) [](https://github.com/sponsors/laurent22/) [](https://www.patreon.com/joplin) [](https://joplinapp.org/donate/#donations)
|
||||||
<!-- DONATELINKS -->
|
<!-- DONATELINKS -->
|
||||||
|
|
||||||
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" style="margin-right:15px"/>
|
<img width="64" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/LinuxIcons/256x256.png" align="left" style="margin-right:15px"/>
|
||||||
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
|||||||
# Sponsors
|
# Sponsors
|
||||||
|
|
||||||
<!-- SPONSORS-ORG -->
|
<!-- SPONSORS-ORG -->
|
||||||
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a> <a href="https://uk.notgamstop.com/bonuses/free-spins-no-deposit-no-gamstop/"><img title="free spins no deposit at NotGamstop" width="256" src="https://joplinapp.org/images/sponsors/NotGamStop.jpg" alt="free spins no deposit at NotGamstop"/></a> <a href="https://www.writemyessay.com/"><img title="writing service for students WriteMyEssay" width="256" src="https://joplinapp.org/images/sponsors/WriteMyEssay.png" alt="writing service for students WriteMyEssay"/></a>
|
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png" alt="topagency"/></a> <a href="https://www.slotozilla.com/nz/no-deposit-bonus"><img title="casino without making any upfront cost" width="256" src="https://joplinapp.org/images/sponsors/Slotozilla.png" alt="casino without making any upfront cost"/></a> <a href="https://writepaper.com/"><img title="best service to write my paper for me" width="256" src="https://joplinapp.org/images/sponsors/WritePaper.png" alt="best service to write my paper for me"/></a> <a href="https://paperwriter.com/"><img title="high-quality paper writing service PaperWriter" width="256" src="https://joplinapp.org/images/sponsors/PaperWriter.png" alt="high-quality paper writing service PaperWriter"/></a> <a href="https://www.bestetf.net/"><img title="BestETF" width="256" src="https://joplinapp.org/images/sponsors/BestEtf.png" alt="BestETF"/></a> <a href="https://freespinny.io/free-spins-no-deposit/"><img title="Freespinny.io Free Spins Bonus site" width="256" src="https://joplinapp.org/images/sponsors/Freespinny.png" alt="Freespinny.io Free Spins Bonus site"/></a> <a href="https://essayshark.com"><img title="EssayShark - essay writers for hire" width="256" src="https://joplinapp.org/images/sponsors/EssayShark.png" alt="EssayShark - essay writers for hire"/></a> <a href="https://pokieslab1.com/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/PokiesLab.png" alt="Australian Real Money Pokies"/></a> <a href="https://pokiesman1.net/real-money-pokies/"><img title="Australian Real Money Pokies" width="256" src="https://joplinapp.org/images/sponsors/Pokiesman.png" alt="Australian Real Money Pokies"/></a> <a href="https://domyessay.com"><img title="Essay writers DoMyEssay are dedicated to providing top-notch, custom-written papers that meet your academic requirements" width="256" src="https://joplinapp.org/images/sponsors/DoMyEssay.png" alt="DoMyEssay"/></a> <a href="https://essaypro.com/"><img title="best essay writing service" width="256" src="https://joplinapp.org/images/sponsors/EssayPro.png" alt="best essay writing service"/></a> <a href="https://socialkings.online"><img title="Boost your reach and buy real followers" width="256" src="https://joplinapp.org/images/sponsors/SocialKings.png" alt="Boost your reach and buy real followers"/></a> <a href="https://uk.notgamstop.com/bonuses/free-spins-no-deposit-no-gamstop/"><img title="free spins no deposit at NotGamstop" width="256" src="https://joplinapp.org/images/sponsors/NotGamStop.jpg" alt="free spins no deposit at NotGamstop"/></a> <a href="https://www.writemyessay.com/"><img title="writing service for students WriteMyEssay" width="256" src="https://joplinapp.org/images/sponsors/WriteMyEssay.png" alt="writing service for students WriteMyEssay"/></a> <a href="https://essayservice.com/"><img title="For those in need of immediate academic assistance, EssayService offers a fast and reliable service to write my essay for me now, ensuring high-quality results within tight deadlines" width="256" src="https://joplinapp.org/images/sponsors/EssayService.png" alt="For those in need of immediate academic assistance, EssayService offers a fast and reliable service to write my essay for me now, ensuring high-quality results within tight deadlines"/></a>
|
||||||
<!-- SPONSORS-ORG -->
|
<!-- SPONSORS-ORG -->
|
||||||
|
|
||||||
* * *
|
* * *
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"vips.dev": {
|
"vips.dev": {
|
||||||
"platforms": ["aarch64-darwin"],
|
"platforms": ["aarch64-darwin"],
|
||||||
},
|
},
|
||||||
"nodejs": "23.11.0",
|
"nodejs": "24.5.0",
|
||||||
"pkg-config": "latest",
|
"pkg-config": "latest",
|
||||||
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
||||||
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"version": "latest",
|
"version": "latest",
|
||||||
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||||
},
|
},
|
||||||
"git": "2.48.1",
|
"git": "2.50.1",
|
||||||
},
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"init_hook": [
|
"init_hook": [
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
postgresql-master:
|
postgresql-master:
|
||||||
image: 'bitnamilegacy/postgresql:17.4.0'
|
image: 'bitnamilegacy/postgresql:17.5.0'
|
||||||
ports:
|
ports:
|
||||||
- '5432:5432'
|
- '5432:5432'
|
||||||
environment:
|
environment:
|
||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
|
- POSTGRESQL_EXTRA_FLAGS=-c work_mem=100000 -c log_statement=all
|
||||||
|
|
||||||
postgresql-slave:
|
postgresql-slave:
|
||||||
image: 'bitnamilegacy/postgresql:17.4.0'
|
image: 'bitnamilegacy/postgresql:17.5.0'
|
||||||
ports:
|
ports:
|
||||||
- '5433:5432'
|
- '5433:5432'
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -76,17 +76,17 @@
|
|||||||
"cspell": "5.21.2",
|
"cspell": "5.21.2",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-interactive": "10.8.0",
|
"eslint-interactive": "10.8.0",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-jest": "27.9.0",
|
"eslint-plugin-jest": "27.9.0",
|
||||||
"eslint-plugin-promise": "6.6.0",
|
"eslint-plugin-promise": "6.6.0",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"execa": "5.1.1",
|
"execa": "5.1.1",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.3.1",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"lerna": "3.22.1",
|
"lerna": "3.22.1",
|
||||||
"lint-staged": "15.5.2",
|
"lint-staged": "16.1.6",
|
||||||
"madge": "8.0.0",
|
"madge": "8.0.0",
|
||||||
"npm-package-json-lint": "8.0.0",
|
"npm-package-json-lint": "8.0.0",
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3"
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"eslint-plugin-github": "4.10.2",
|
"eslint-plugin-github": "4.10.2",
|
||||||
"http-server": "14.1.1",
|
"http-server": "14.1.1",
|
||||||
"node-gyp": "11.2.0",
|
"node-gyp": "11.3.0",
|
||||||
"nodemon": "3.1.10"
|
"nodemon": "3.1.10"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.2",
|
"packageManager": "yarn@4.9.2",
|
||||||
|
|||||||
270
packages/app-cli/app/cli-integration-tests.test.ts
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import * as fs from 'fs-extra';
|
||||||
|
import Logger, { TargetType } from '@joplin/utils/Logger';
|
||||||
|
import { dirname } from '@joplin/lib/path-utils';
|
||||||
|
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
|
||||||
|
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
||||||
|
import BaseModel from '@joplin/lib/BaseModel';
|
||||||
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import { node } from 'execa';
|
||||||
|
import { splitCommandString } from '@joplin/utils';
|
||||||
|
const nodeSqlite = require('sqlite3');
|
||||||
|
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
|
||||||
|
const { default: shimInitCli } = require('./utils/shimInitCli');
|
||||||
|
|
||||||
|
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
|
||||||
|
const joplinAppPath = `${__dirname}/main.js`;
|
||||||
|
|
||||||
|
shimInitCli({ nodeSqlite, appVersion: () => require('../package.json').version, keytar: null });
|
||||||
|
require('@joplin/lib/testing/test-utils');
|
||||||
|
|
||||||
|
|
||||||
|
interface Client {
|
||||||
|
id: number;
|
||||||
|
profileDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClient(id: number): Client {
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
profileDir: `${baseDir}/client${id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function execCommand(client: Client, command: string) {
|
||||||
|
const result = await node(
|
||||||
|
joplinAppPath,
|
||||||
|
['--update-geolocation-disabled', '--env', 'dev', '--profile', client.profileDir, ...splitCommandString(command)],
|
||||||
|
);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(`Command failed: ${command}:\nstderr: ${result.stderr}\nstdout: ${result.stdout}`);
|
||||||
|
}
|
||||||
|
return result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearDatabase(db: JoplinDatabase) {
|
||||||
|
await db.transactionExecBatch(['DELETE FROM folders', 'DELETE FROM notes', 'DELETE FROM tags', 'DELETE FROM note_tags', 'DELETE FROM resources', 'DELETE FROM deleted_items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe('cli-integration-tests', () => {
|
||||||
|
let client: Client;
|
||||||
|
let db: JoplinDatabase;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await fs.remove(baseDir);
|
||||||
|
await fs.mkdir(baseDir);
|
||||||
|
|
||||||
|
client = createClient(1);
|
||||||
|
// Initialize the database by running a client command and exiting.
|
||||||
|
await execCommand(client, 'version');
|
||||||
|
|
||||||
|
const dbLogger = new Logger();
|
||||||
|
dbLogger.addTarget(TargetType.Console);
|
||||||
|
dbLogger.setLevel(Logger.LEVEL_WARN);
|
||||||
|
|
||||||
|
db = new JoplinDatabase(new DatabaseDriverNode());
|
||||||
|
db.setLogger(dbLogger);
|
||||||
|
|
||||||
|
await db.open({ name: `${client.profileDir}/database.sqlite` });
|
||||||
|
BaseModel.setDb(db);
|
||||||
|
Setting.setConstant('rootProfileDir', client.profileDir);
|
||||||
|
Setting.setConstant('profileDir', client.profileDir);
|
||||||
|
await loadKeychainServiceAndSettings([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await clearDatabase(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
'version',
|
||||||
|
'help',
|
||||||
|
])('should run command %j without crashing', async (command) => {
|
||||||
|
await execCommand(client, command);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support the \'ls\' command', async () => {
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
await execCommand(client, 'mknote note1');
|
||||||
|
await execCommand(client, 'mknote note2');
|
||||||
|
const r = await execCommand(client, 'ls');
|
||||||
|
|
||||||
|
expect(r.indexOf('note1') >= 0).toBe(true);
|
||||||
|
expect(r.indexOf('note2') >= 0).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support the \'mv\' command', async () => {
|
||||||
|
await execCommand(client, 'mkbook nb2');
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
await execCommand(client, 'mknote n1');
|
||||||
|
await execCommand(client, 'mv n1 nb2');
|
||||||
|
|
||||||
|
const f1 = await Folder.loadByTitle('nb1');
|
||||||
|
const f2 = await Folder.loadByTitle('nb2');
|
||||||
|
let notes1 = await Note.previews(f1.id);
|
||||||
|
let notes2 = await Note.previews(f2.id);
|
||||||
|
|
||||||
|
expect(notes1.length).toBe(0);
|
||||||
|
expect(notes2.length).toBe(1);
|
||||||
|
|
||||||
|
await execCommand(client, 'mknote note1');
|
||||||
|
await execCommand(client, 'mknote note2');
|
||||||
|
await execCommand(client, 'mknote note3');
|
||||||
|
await execCommand(client, 'mknote blabla');
|
||||||
|
|
||||||
|
notes1 = await Note.previews(f1.id);
|
||||||
|
notes2 = await Note.previews(f2.id);
|
||||||
|
|
||||||
|
expect(notes1.length).toBe(4);
|
||||||
|
expect(notes2.length).toBe(1);
|
||||||
|
|
||||||
|
await execCommand(client, 'mv \'note*\' nb2');
|
||||||
|
|
||||||
|
notes2 = await Note.previews(f2.id);
|
||||||
|
notes1 = await Note.previews(f1.id);
|
||||||
|
|
||||||
|
expect(notes1.length).toBe(1);
|
||||||
|
expect(notes2.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support the \'use\' command', async () => {
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
await execCommand(client, 'mkbook nb2');
|
||||||
|
await execCommand(client, 'mknote n1');
|
||||||
|
await execCommand(client, 'mknote n2');
|
||||||
|
|
||||||
|
const f1 = await Folder.loadByTitle('nb1');
|
||||||
|
const f2 = await Folder.loadByTitle('nb2');
|
||||||
|
let notes1 = await Note.previews(f1.id);
|
||||||
|
let notes2 = await Note.previews(f2.id);
|
||||||
|
|
||||||
|
expect(notes1.length).toBe(0);
|
||||||
|
expect(notes2.length).toBe(2);
|
||||||
|
|
||||||
|
await execCommand(client, 'use nb1');
|
||||||
|
await execCommand(client, 'mknote note2');
|
||||||
|
await execCommand(client, 'mknote note3');
|
||||||
|
|
||||||
|
notes1 = await Note.previews(f1.id);
|
||||||
|
notes2 = await Note.previews(f2.id);
|
||||||
|
|
||||||
|
expect(notes1.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support creating and removing folders', async () => {
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
|
||||||
|
let folders = await Folder.all();
|
||||||
|
expect(folders.length).toBe(1);
|
||||||
|
expect(folders[0].title).toBe('nb1');
|
||||||
|
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
|
||||||
|
folders = await Folder.all();
|
||||||
|
expect(folders.length).toBe(2);
|
||||||
|
expect(folders[0].title).toBe('nb1');
|
||||||
|
expect(folders[1].title).toBe('nb1');
|
||||||
|
|
||||||
|
await execCommand(client, 'rmbook -p -f nb1');
|
||||||
|
|
||||||
|
folders = await Folder.all();
|
||||||
|
expect(folders.length).toBe(1);
|
||||||
|
|
||||||
|
await execCommand(client, 'rmbook -p -f nb1');
|
||||||
|
|
||||||
|
folders = await Folder.all();
|
||||||
|
expect(folders.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support creating and removing notes', async () => {
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
await execCommand(client, 'mknote n1');
|
||||||
|
|
||||||
|
let notes = await Note.all();
|
||||||
|
expect(notes.length).toBe(1);
|
||||||
|
expect(notes[0].title).toBe('n1');
|
||||||
|
|
||||||
|
await execCommand(client, 'rmnote -p -f n1');
|
||||||
|
notes = await Note.all();
|
||||||
|
expect(notes.length).toBe(0);
|
||||||
|
|
||||||
|
await execCommand(client, 'mknote n1');
|
||||||
|
await execCommand(client, 'mknote n2');
|
||||||
|
|
||||||
|
notes = await Note.all();
|
||||||
|
expect(notes.length).toBe(2);
|
||||||
|
|
||||||
|
// Should fail to delete a non-existent note
|
||||||
|
let failed = false;
|
||||||
|
try {
|
||||||
|
await execCommand(client, 'rmnote -f \'blabla*\'');
|
||||||
|
} catch (error) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
expect(failed).toBe(true);
|
||||||
|
|
||||||
|
notes = await Note.all();
|
||||||
|
expect(notes.length).toBe(2);
|
||||||
|
|
||||||
|
await execCommand(client, 'rmnote -f -p \'n*\'');
|
||||||
|
|
||||||
|
notes = await Note.all();
|
||||||
|
expect(notes.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support listing the contents of notes', async () => {
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
await execCommand(client, 'mknote mynote');
|
||||||
|
|
||||||
|
const folder = await Folder.loadByTitle('nb1');
|
||||||
|
const note = await Note.loadFolderNoteByField(folder.id, 'title', 'mynote');
|
||||||
|
|
||||||
|
let r = await execCommand(client, 'cat mynote');
|
||||||
|
expect(r).toContain('mynote');
|
||||||
|
expect(r).not.toContain(note.id);
|
||||||
|
|
||||||
|
r = await execCommand(client, 'cat -v mynote');
|
||||||
|
expect(r).toContain(note.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support changing settings with config', async () => {
|
||||||
|
await execCommand(client, 'config editor vim');
|
||||||
|
await Setting.reset();
|
||||||
|
await Setting.load();
|
||||||
|
expect(Setting.value('editor')).toBe('vim');
|
||||||
|
|
||||||
|
await execCommand(client, 'config editor subl');
|
||||||
|
await Setting.reset();
|
||||||
|
await Setting.load();
|
||||||
|
expect(Setting.value('editor')).toBe('subl');
|
||||||
|
|
||||||
|
const r = await execCommand(client, 'config');
|
||||||
|
expect(r.indexOf('editor') >= 0).toBe(true);
|
||||||
|
expect(r.indexOf('subl') >= 0).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support copying folders with cp', async () => {
|
||||||
|
await execCommand(client, 'mkbook nb2');
|
||||||
|
await execCommand(client, 'mkbook nb1');
|
||||||
|
await execCommand(client, 'mknote n1');
|
||||||
|
|
||||||
|
await execCommand(client, 'cp n1');
|
||||||
|
|
||||||
|
const f1 = await Folder.loadByTitle('nb1');
|
||||||
|
const f2 = await Folder.loadByTitle('nb2');
|
||||||
|
let notes = await Note.previews(f1.id);
|
||||||
|
|
||||||
|
expect(notes.length).toBe(2);
|
||||||
|
|
||||||
|
await execCommand(client, 'cp n1 nb2');
|
||||||
|
const notesF1 = await Note.previews(f1.id);
|
||||||
|
expect(notesF1.length).toBe(2);
|
||||||
|
notes = await Note.previews(f2.id);
|
||||||
|
expect(notes.length).toBe(1);
|
||||||
|
expect(notes[0].title).toBe(notesF1[0].title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import * as fs from 'fs-extra';
|
|
||||||
import Logger, { TargetType } from '@joplin/utils/Logger';
|
|
||||||
import { dirname } from '@joplin/lib/path-utils';
|
|
||||||
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
|
|
||||||
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
|
|
||||||
import BaseModel from '@joplin/lib/BaseModel';
|
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
|
||||||
import Note from '@joplin/lib/models/Note';
|
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
|
||||||
const { sprintf } = require('sprintf-js');
|
|
||||||
const exec = require('child_process').exec;
|
|
||||||
const nodeSqlite = require('sqlite3');
|
|
||||||
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
|
|
||||||
const { default: shimInitCli } = require('./utils/shimInitCli');
|
|
||||||
|
|
||||||
const baseDir = `${dirname(__dirname)}/tests/cli-integration`;
|
|
||||||
const joplinAppPath = `${__dirname}/main.js`;
|
|
||||||
|
|
||||||
shimInitCli({ nodeSqlite, appVersion: () => require('../package.json').version, keytar: null });
|
|
||||||
require('@joplin/lib/testing/test-utils');
|
|
||||||
|
|
||||||
const logger = new Logger();
|
|
||||||
logger.addTarget(TargetType.Console);
|
|
||||||
logger.setLevel(Logger.LEVEL_ERROR);
|
|
||||||
|
|
||||||
const dbLogger = new Logger();
|
|
||||||
dbLogger.addTarget(TargetType.Console);
|
|
||||||
dbLogger.setLevel(Logger.LEVEL_INFO);
|
|
||||||
|
|
||||||
const db = new JoplinDatabase(new DatabaseDriverNode());
|
|
||||||
db.setLogger(dbLogger);
|
|
||||||
|
|
||||||
interface Client {
|
|
||||||
id: number;
|
|
||||||
profileDir: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createClient(id: number): Client {
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
profileDir: `${baseDir}/client${id}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = createClient(1);
|
|
||||||
|
|
||||||
function execCommand(client: Client, command: string) {
|
|
||||||
const exePath = `node ${joplinAppPath}`;
|
|
||||||
const cmd = `${exePath} --update-geolocation-disabled --env dev --profile ${client.profileDir} ${command}`;
|
|
||||||
logger.info(`${client.id}: ${command}`);
|
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
exec(cmd, (error: string, stdout: string, stderr: string) => {
|
|
||||||
if (error) {
|
|
||||||
logger.error(stderr);
|
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
resolve(stdout.trim());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertTrue(v: unknown) {
|
|
||||||
if (!v) throw new Error(sprintf('Expected "true", got "%s"."', v));
|
|
||||||
process.stdout.write('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertFalse(v: unknown) {
|
|
||||||
if (v) throw new Error(sprintf('Expected "false", got "%s"."', v));
|
|
||||||
process.stdout.write('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertEquals(expected: unknown, real: unknown) {
|
|
||||||
if (expected !== real) throw new Error(sprintf('Expecting "%s", got "%s"', expected, real));
|
|
||||||
process.stdout.write('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearDatabase() {
|
|
||||||
await db.transactionExecBatch(['DELETE FROM folders', 'DELETE FROM notes', 'DELETE FROM tags', 'DELETE FROM note_tags', 'DELETE FROM resources', 'DELETE FROM deleted_items']);
|
|
||||||
}
|
|
||||||
|
|
||||||
const testUnits: Record<string, ()=> Promise<void>> = {};
|
|
||||||
|
|
||||||
testUnits.testFolders = async () => {
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
|
|
||||||
let folders = await Folder.all();
|
|
||||||
assertEquals(1, folders.length);
|
|
||||||
assertEquals('nb1', folders[0].title);
|
|
||||||
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
|
|
||||||
folders = await Folder.all();
|
|
||||||
assertEquals(2, folders.length);
|
|
||||||
assertEquals('nb1', folders[0].title);
|
|
||||||
assertEquals('nb1', folders[1].title);
|
|
||||||
|
|
||||||
await execCommand(client, 'rmbook -p -f nb1');
|
|
||||||
|
|
||||||
folders = await Folder.all();
|
|
||||||
assertEquals(1, folders.length);
|
|
||||||
|
|
||||||
await execCommand(client, 'rmbook -p -f nb1');
|
|
||||||
|
|
||||||
folders = await Folder.all();
|
|
||||||
assertEquals(0, folders.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
testUnits.testNotes = async () => {
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
await execCommand(client, 'mknote n1');
|
|
||||||
|
|
||||||
let notes = await Note.all();
|
|
||||||
assertEquals(1, notes.length);
|
|
||||||
assertEquals('n1', notes[0].title);
|
|
||||||
|
|
||||||
await execCommand(client, 'rmnote -p -f n1');
|
|
||||||
notes = await Note.all();
|
|
||||||
assertEquals(0, notes.length);
|
|
||||||
|
|
||||||
await execCommand(client, 'mknote n1');
|
|
||||||
await execCommand(client, 'mknote n2');
|
|
||||||
|
|
||||||
notes = await Note.all();
|
|
||||||
assertEquals(2, notes.length);
|
|
||||||
|
|
||||||
// Should fail to delete a non-existent note
|
|
||||||
let failed = false;
|
|
||||||
try {
|
|
||||||
await execCommand(client, 'rmnote -f \'blabla*\'');
|
|
||||||
} catch (error) {
|
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
assertEquals(failed, true);
|
|
||||||
|
|
||||||
notes = await Note.all();
|
|
||||||
assertEquals(2, notes.length);
|
|
||||||
|
|
||||||
await execCommand(client, 'rmnote -f -p \'n*\'');
|
|
||||||
|
|
||||||
notes = await Note.all();
|
|
||||||
assertEquals(0, notes.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
testUnits.testCat = async () => {
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
await execCommand(client, 'mknote mynote');
|
|
||||||
|
|
||||||
const folder = await Folder.loadByTitle('nb1');
|
|
||||||
const note = await Note.loadFolderNoteByField(folder.id, 'title', 'mynote');
|
|
||||||
|
|
||||||
let r = await execCommand(client, 'cat mynote');
|
|
||||||
assertTrue(r.indexOf('mynote') >= 0);
|
|
||||||
assertFalse(r.indexOf(note.id) >= 0);
|
|
||||||
|
|
||||||
r = await execCommand(client, 'cat -v mynote');
|
|
||||||
assertTrue(r.indexOf(note.id) >= 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
testUnits.testConfig = async () => {
|
|
||||||
await execCommand(client, 'config editor vim');
|
|
||||||
await Setting.reset();
|
|
||||||
await Setting.load();
|
|
||||||
assertEquals('vim', Setting.value('editor'));
|
|
||||||
|
|
||||||
await execCommand(client, 'config editor subl');
|
|
||||||
await Setting.reset();
|
|
||||||
await Setting.load();
|
|
||||||
assertEquals('subl', Setting.value('editor'));
|
|
||||||
|
|
||||||
const r = await execCommand(client, 'config');
|
|
||||||
assertTrue(r.indexOf('editor') >= 0);
|
|
||||||
assertTrue(r.indexOf('subl') >= 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
testUnits.testCp = async () => {
|
|
||||||
await execCommand(client, 'mkbook nb2');
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
await execCommand(client, 'mknote n1');
|
|
||||||
|
|
||||||
await execCommand(client, 'cp n1');
|
|
||||||
|
|
||||||
const f1 = await Folder.loadByTitle('nb1');
|
|
||||||
const f2 = await Folder.loadByTitle('nb2');
|
|
||||||
let notes = await Note.previews(f1.id);
|
|
||||||
|
|
||||||
assertEquals(2, notes.length);
|
|
||||||
|
|
||||||
await execCommand(client, 'cp n1 nb2');
|
|
||||||
const notesF1 = await Note.previews(f1.id);
|
|
||||||
assertEquals(2, notesF1.length);
|
|
||||||
notes = await Note.previews(f2.id);
|
|
||||||
assertEquals(1, notes.length);
|
|
||||||
assertEquals(notesF1[0].title, notes[0].title);
|
|
||||||
};
|
|
||||||
|
|
||||||
testUnits.testLs = async () => {
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
await execCommand(client, 'mknote note1');
|
|
||||||
await execCommand(client, 'mknote note2');
|
|
||||||
const r = await execCommand(client, 'ls');
|
|
||||||
|
|
||||||
assertTrue(r.indexOf('note1') >= 0);
|
|
||||||
assertTrue(r.indexOf('note2') >= 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
testUnits.testMv = async () => {
|
|
||||||
await execCommand(client, 'mkbook nb2');
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
await execCommand(client, 'mknote n1');
|
|
||||||
await execCommand(client, 'mv n1 nb2');
|
|
||||||
|
|
||||||
const f1 = await Folder.loadByTitle('nb1');
|
|
||||||
const f2 = await Folder.loadByTitle('nb2');
|
|
||||||
let notes1 = await Note.previews(f1.id);
|
|
||||||
let notes2 = await Note.previews(f2.id);
|
|
||||||
|
|
||||||
assertEquals(0, notes1.length);
|
|
||||||
assertEquals(1, notes2.length);
|
|
||||||
|
|
||||||
await execCommand(client, 'mknote note1');
|
|
||||||
await execCommand(client, 'mknote note2');
|
|
||||||
await execCommand(client, 'mknote note3');
|
|
||||||
await execCommand(client, 'mknote blabla');
|
|
||||||
|
|
||||||
notes1 = await Note.previews(f1.id);
|
|
||||||
notes2 = await Note.previews(f2.id);
|
|
||||||
|
|
||||||
assertEquals(4, notes1.length);
|
|
||||||
assertEquals(1, notes2.length);
|
|
||||||
|
|
||||||
await execCommand(client, 'mv \'note*\' nb2');
|
|
||||||
|
|
||||||
notes2 = await Note.previews(f2.id);
|
|
||||||
notes1 = await Note.previews(f1.id);
|
|
||||||
|
|
||||||
assertEquals(1, notes1.length);
|
|
||||||
assertEquals(4, notes2.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
testUnits.testUse = async () => {
|
|
||||||
await execCommand(client, 'mkbook nb1');
|
|
||||||
await execCommand(client, 'mkbook nb2');
|
|
||||||
await execCommand(client, 'mknote n1');
|
|
||||||
await execCommand(client, 'mknote n2');
|
|
||||||
|
|
||||||
const f1 = await Folder.loadByTitle('nb1');
|
|
||||||
const f2 = await Folder.loadByTitle('nb2');
|
|
||||||
let notes1 = await Note.previews(f1.id);
|
|
||||||
let notes2 = await Note.previews(f2.id);
|
|
||||||
|
|
||||||
assertEquals(0, notes1.length);
|
|
||||||
assertEquals(2, notes2.length);
|
|
||||||
|
|
||||||
await execCommand(client, 'use nb1');
|
|
||||||
await execCommand(client, 'mknote note2');
|
|
||||||
await execCommand(client, 'mknote note3');
|
|
||||||
|
|
||||||
notes1 = await Note.previews(f1.id);
|
|
||||||
notes2 = await Note.previews(f2.id);
|
|
||||||
|
|
||||||
assertEquals(2, notes1.length);
|
|
||||||
assertEquals(2, notes2.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
await fs.remove(baseDir);
|
|
||||||
|
|
||||||
logger.info(await execCommand(client, 'version'));
|
|
||||||
|
|
||||||
await db.open({ name: `${client.profileDir}/database.sqlite` });
|
|
||||||
BaseModel.setDb(db);
|
|
||||||
Setting.setConstant('rootProfileDir', client.profileDir);
|
|
||||||
Setting.setConstant('profileDir', client.profileDir);
|
|
||||||
await loadKeychainServiceAndSettings([]);
|
|
||||||
|
|
||||||
let onlyThisTest = 'testMv';
|
|
||||||
onlyThisTest = '';
|
|
||||||
|
|
||||||
for (const n in testUnits) {
|
|
||||||
if (!testUnits.hasOwnProperty(n)) continue;
|
|
||||||
if (onlyThisTest && n !== onlyThisTest) continue;
|
|
||||||
|
|
||||||
await clearDatabase();
|
|
||||||
const testName = n.substr(4).toLowerCase();
|
|
||||||
process.stdout.write(`${testName}: `);
|
|
||||||
await testUnits[n]();
|
|
||||||
console.info('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(error => {
|
|
||||||
console.info('');
|
|
||||||
logger.error(error);
|
|
||||||
});
|
|
||||||
@@ -12,7 +12,7 @@ class Command extends BaseCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override async action() {
|
public override async action() {
|
||||||
this.stdout(versionInfo(require('./package.json'), {}).message);
|
this.stdout(versionInfo(require('../package.json'), {}).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import app from '../app';
|
|||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import BaseCommand from '../base-command';
|
import BaseCommand from '../base-command';
|
||||||
import setupCommand from '../setupCommand';
|
import setupCommand from '../setupCommand';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||||
export const setupCommandForTesting = (CommandClass: any, stdout: Function = null): BaseCommand => {
|
export const setupCommandForTesting = (CommandClass: any, stdout: Function = null): BaseCommand => {
|
||||||
@@ -18,4 +19,9 @@ export const setupApplication = async () => {
|
|||||||
|
|
||||||
// Some tests also need access to the Redux store
|
// Some tests also need access to the Redux store
|
||||||
app().initRedux();
|
app().initRedux();
|
||||||
|
|
||||||
|
// Since the settings need to be loaded before the store is created, it will never
|
||||||
|
// receive the SETTING_UPDATE_ALL event, which means state.settings will not be
|
||||||
|
// initialised. So we manually call dispatchUpdateAll() to force an update.
|
||||||
|
Setting.dispatchUpdateAll();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { afterEachCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
const { afterEachCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
const { default: shimInitCli } = require('./app/utils/shimInitCli');
|
||||||
const shim = require('@joplin/lib/shim').default;
|
const shim = require('@joplin/lib/shim').default;
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
const nodeSqlite = require('sqlite3');
|
const nodeSqlite = require('sqlite3');
|
||||||
@@ -13,7 +13,7 @@ try {
|
|||||||
keytar = null;
|
keytar = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
shimInit({ sharp, keytar, nodeSqlite });
|
shimInitCli({ sharp, nodeSqlite, appVersion: () => require('./package.json').version, keytar });
|
||||||
|
|
||||||
global.afterEach(async () => {
|
global.afterEach(async () => {
|
||||||
await afterEachCleanUp();
|
await afterEachCleanUp();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"compare-version": "0.1.2",
|
"compare-version": "0.1.2",
|
||||||
"file-type": "16.5.4",
|
"file-type": "16.5.4",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.3.1",
|
||||||
"html-entities": "1.4.0",
|
"html-entities": "1.4.0",
|
||||||
"keytar": "7.9.0",
|
"keytar": "7.9.0",
|
||||||
"md5": "2.3.0",
|
"md5": "2.3.0",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ describe('MarkupToHtml', () => {
|
|||||||
pluginAssets: [],
|
pluginAssets: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(await service.render(MarkupLanguage.Html, testString, {}, {})).toMatchObject(expectedOutput);
|
expect(await service.render(MarkupLanguage.Html, testString, {}, { })).toMatchObject(expectedOutput);
|
||||||
expect(await service.render(MarkupLanguage.Markdown, testString, {}, {})).toMatchObject(expectedOutput);
|
expect(await service.render(MarkupLanguage.Markdown, testString, {}, { })).toMatchObject(expectedOutput);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<ul>
|
||||||
|
<li><a href="https://example.com/" title="This
|
||||||
|
|
||||||
|
is a test title
|
||||||
|
testing!
|
||||||
|
|
||||||
|
Test...">Test!</a></li>
|
||||||
|
<li><a href="http://example.com" title="
|
||||||
|
Test
|
||||||
|
">Another test...</a></li>
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- [Test!](https://example.com/ "This
|
||||||
|
is a test title
|
||||||
|
testing!
|
||||||
|
Test...")
|
||||||
|
- [Another test...](http://example.com "Test")
|
||||||
BIN
packages/app-cli/tests/support/onenote/Math.one
Normal file
3
packages/app-cli/tests/support/test_notes/md/long-url.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# test for joplin import
|
||||||
|
|
||||||
|
[https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm\_source%3DYouTube%2520Instagram%26utm\_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm\_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm\_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm\_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm\_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o\_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&\_\_tn\_\_=H-y-R&c[0]=AT0eE6OXx\_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887\_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG\_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo76663333hhsgsu](<https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm_source%3DYouTube%2520Instagram%26utm_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&__tn__=H-y-R&c[0]=AT0eE6OXx_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo>)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
id: 20250821081408
|
||||||
|
date: 2025-08-21
|
||||||
|
keywords:
|
||||||
|
---
|
||||||
|
|
||||||
|
# A test file for Joplin importer
|
||||||
|
|
||||||
|
Test
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: test
|
||||||
|
created: 2025-07-22 17:30:44Z
|
||||||
|
updated: 2025-07-22 17:37:48Z
|
||||||
|
---
|
||||||
|
|
||||||
|
test
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Frontmatter test"
|
title: "Frontmatter test"
|
||||||
created_at: 01-01-2024 01:23 AM
|
created_at: 01-01-2024 01:23 AM
|
||||||
updated_at: 02-01-2024 04:56 AM
|
updated_at: 01-01-2024 04:56 AM
|
||||||
---
|
---
|
||||||
|
|
||||||
# Frontmatter test
|
# Frontmatter test
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { defaultWindowId } from '@joplin/lib/reducer';
|
|||||||
import { msleep, Second } from '@joplin/utils/time';
|
import { msleep, Second } from '@joplin/utils/time';
|
||||||
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';
|
import determineBaseAppDirs from '@joplin/lib/determineBaseAppDirs';
|
||||||
import getAppName from '@joplin/lib/getAppName';
|
import getAppName from '@joplin/lib/getAppName';
|
||||||
|
import { execCommand } from '@joplin/utils';
|
||||||
|
|
||||||
interface RendererProcessQuitReply {
|
interface RendererProcessQuitReply {
|
||||||
canClose: boolean;
|
canClose: boolean;
|
||||||
@@ -810,6 +811,33 @@ export default class ElectronAppWrapper {
|
|||||||
return this.customProtocolHandler_;
|
return this.customProtocolHandler_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fixLinuxAccessibility_() {
|
||||||
|
if (this.electronApp().accessibilitySupportEnabled) return;
|
||||||
|
|
||||||
|
const isOrcaRunning = async () => {
|
||||||
|
if (!shim.isLinux()) return false;
|
||||||
|
try {
|
||||||
|
const matchingProcesses = await execCommand(['ps', '--no-headers', '-C', 'orca'], { quiet: true });
|
||||||
|
return matchingProcesses.trim().length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.stderr || error.exitCode !== 1) {
|
||||||
|
// eslint-disable-next-line no-console -- The main logger is not available at this point.
|
||||||
|
console.error('Failed to check for and enable accessibility support:', error.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Work around https://issues.chromium.org/issues/431257156 by force-enabling accessibility
|
||||||
|
// when Orca (a screen reader) is running:
|
||||||
|
if (await isOrcaRunning()) {
|
||||||
|
// eslint-disable-next-line no-console -- The main logger is not available at this point.
|
||||||
|
console.log('Linux accessibility: Enabling full accessibility support.');
|
||||||
|
this.electronApp().setAccessibilitySupportEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
// Since we are doing other async things before creating the window, we might miss
|
// Since we are doing other async things before creating the window, we might miss
|
||||||
// the "ready" event. So we use the function below to make sure that the app is ready.
|
// the "ready" event. So we use the function below to make sure that the app is ready.
|
||||||
@@ -818,6 +846,8 @@ export default class ElectronAppWrapper {
|
|||||||
const alreadyRunning = await this.ensureSingleInstance();
|
const alreadyRunning = await this.ensureSingleInstance();
|
||||||
if (alreadyRunning) return;
|
if (alreadyRunning) return;
|
||||||
|
|
||||||
|
await this.fixLinuxAccessibility_();
|
||||||
|
|
||||||
this.customProtocolHandler_ = handleCustomProtocols();
|
this.customProtocolHandler_ = handleCustomProtocols();
|
||||||
this.createWindow();
|
this.createWindow();
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Setting from '@joplin/lib/models/Setting';
|
|||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
const { friendlySafeFilename } = require('@joplin/lib/path-utils');
|
const { friendlySafeFilename } = require('@joplin/lib/path-utils');
|
||||||
import time from '@joplin/lib/time';
|
import time from '@joplin/lib/time';
|
||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
|
||||||
@@ -62,8 +62,10 @@ export default class InteropServiceHelper {
|
|||||||
|
|
||||||
htmlFile = await this.exportNoteToHtmlFile(noteId, exportOptions);
|
htmlFile = await this.exportNoteToHtmlFile(noteId, exportOptions);
|
||||||
|
|
||||||
const windowOptions = {
|
const windowOptions: BrowserWindowConstructorOptions = {
|
||||||
show: false,
|
// Work around a printing issue: As of Electron 39, if the window is initially hidden, printing crashes the app.
|
||||||
|
// This only seems to be necessary on Linux.
|
||||||
|
show: shim.isLinux(),
|
||||||
};
|
};
|
||||||
|
|
||||||
win = bridge().newBrowserWindow(windowOptions);
|
win = bridge().newBrowserWindow(windowOptions);
|
||||||
@@ -120,6 +122,9 @@ export default class InteropServiceHelper {
|
|||||||
//
|
//
|
||||||
// 2025-05-03: Windows and MacOS also need the window.print() workaround.
|
// 2025-05-03: Windows and MacOS also need the window.print() workaround.
|
||||||
// See https://github.com/electron/electron/pull/46937.
|
// See https://github.com/electron/electron/pull/46937.
|
||||||
|
//
|
||||||
|
// 2025-10-30: window.print() now causes a crash on Linux -- switch back to the
|
||||||
|
// other method.
|
||||||
|
|
||||||
const applyWorkaround = true;
|
const applyWorkaround = true;
|
||||||
if (applyWorkaround) {
|
if (applyWorkaround) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ describe('app.reducer', () => {
|
|||||||
...createAppDefaultState({}),
|
...createAppDefaultState({}),
|
||||||
backgroundWindows: {
|
backgroundWindows: {
|
||||||
testWindow: {
|
testWindow: {
|
||||||
...createAppDefaultWindowState(null),
|
...createAppDefaultWindowState(),
|
||||||
windowId: 'testWindow',
|
windowId: 'testWindow',
|
||||||
|
|
||||||
visibleDialogs: {
|
visibleDialogs: {
|
||||||
|
|||||||
@@ -30,17 +30,6 @@ export interface NoteIdToScrollPercent {
|
|||||||
[noteId: string]: number;
|
[noteId: string]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RichTextEditorSelectionBookmark = unknown;
|
|
||||||
|
|
||||||
export interface EditorCursorLocations {
|
|
||||||
readonly richText?: RichTextEditorSelectionBookmark;
|
|
||||||
readonly markdown?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NoteIdToEditorCursorLocations {
|
|
||||||
[noteId: string]: EditorCursorLocations;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VisibleDialogs {
|
export interface VisibleDialogs {
|
||||||
[dialogKey: string]: boolean;
|
[dialogKey: string]: boolean;
|
||||||
}
|
}
|
||||||
@@ -53,9 +42,6 @@ export interface AppWindowState extends WindowState {
|
|||||||
devToolsVisible: boolean;
|
devToolsVisible: boolean;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
watchedResources: any;
|
watchedResources: any;
|
||||||
|
|
||||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
|
||||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackgroundWindowStates {
|
interface BackgroundWindowStates {
|
||||||
@@ -79,7 +65,7 @@ export interface AppState extends State, AppWindowState {
|
|||||||
isResettingLayout: boolean;
|
isResettingLayout: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createAppDefaultWindowState = (globalState: AppState|null): AppWindowState => {
|
export const createAppDefaultWindowState = (): AppWindowState => {
|
||||||
return {
|
return {
|
||||||
...defaultWindowState,
|
...defaultWindowState,
|
||||||
visibleDialogs: {},
|
visibleDialogs: {},
|
||||||
@@ -88,12 +74,6 @@ export const createAppDefaultWindowState = (globalState: AppState|null): AppWind
|
|||||||
editorCodeView: true,
|
editorCodeView: true,
|
||||||
devToolsVisible: false,
|
devToolsVisible: false,
|
||||||
watchedResources: {},
|
watchedResources: {},
|
||||||
|
|
||||||
// Maintain the scroll and cursor location for secondary windows separate from the
|
|
||||||
// main window. This prevents scrolling in a secondary window from changing/resetting
|
|
||||||
// the default scroll position in the main window:
|
|
||||||
lastEditorCursorLocations: globalState?.lastEditorCursorLocations ?? {},
|
|
||||||
lastEditorScrollPercents: globalState?.lastEditorScrollPercents ?? {},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,7 +81,7 @@ export const createAppDefaultWindowState = (globalState: AppState|null): AppWind
|
|||||||
export function createAppDefaultState(resourceEditWatcherDefaultState: any): AppState {
|
export function createAppDefaultState(resourceEditWatcherDefaultState: any): AppState {
|
||||||
return {
|
return {
|
||||||
...defaultState,
|
...defaultState,
|
||||||
...createAppDefaultWindowState(null),
|
...createAppDefaultWindowState(),
|
||||||
route: {
|
route: {
|
||||||
type: 'NAV_GO',
|
type: 'NAV_GO',
|
||||||
routeName: 'Main',
|
routeName: 'Main',
|
||||||
@@ -307,28 +287,6 @@ export default function(state: AppState, action: any) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'EDITOR_SCROLL_PERCENT_SET':
|
|
||||||
|
|
||||||
{
|
|
||||||
newState = { ...state };
|
|
||||||
const newPercents = { ...newState.lastEditorScrollPercents };
|
|
||||||
newPercents[action.noteId] = action.percent;
|
|
||||||
newState.lastEditorScrollPercents = newPercents;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'EDITOR_CURSOR_POSITION_SET':
|
|
||||||
{
|
|
||||||
newState = { ...state };
|
|
||||||
const newCursorLocations = { ...newState.lastEditorCursorLocations };
|
|
||||||
newCursorLocations[action.noteId] = {
|
|
||||||
...(newCursorLocations[action.noteId] ?? {}),
|
|
||||||
...action.location,
|
|
||||||
};
|
|
||||||
newState.lastEditorCursorLocations = newCursorLocations;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||||
newState = { ...state };
|
newState = { ...state };
|
||||||
newState.devToolsVisible = !newState.devToolsVisible;
|
newState.devToolsVisible = !newState.devToolsVisible;
|
||||||
|
|||||||
@@ -280,6 +280,18 @@ class Application extends BaseApplication {
|
|||||||
Setting.setValue('plugins.states', pluginSettings);
|
Setting.setValue('plugins.states', pluginSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As of Joplin 3.5.7, the ABC rendering is part of the app so we automatically disable the plugin
|
||||||
|
if (pluginSettings['org.joplinapp.plugins.AbcSheetMusic']) {
|
||||||
|
pluginSettings = {
|
||||||
|
...pluginSettings,
|
||||||
|
['org.joplinapp.plugins.AbcSheetMusic']: {
|
||||||
|
enabled: false,
|
||||||
|
deleted: false,
|
||||||
|
hasBeenUpdated: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
|
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
|
||||||
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
|
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import { _ } from '@joplin/lib/locale';
|
|
||||||
import Note from '@joplin/lib/models/Note';
|
|
||||||
import { stateUtils } from '@joplin/lib/reducer';
|
|
||||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
|
||||||
import { MarkupLanguage } from '@joplin/renderer';
|
|
||||||
import { runtime as convertHtmlToMarkdown } from '@joplin/lib/commands/convertHtmlToMarkdown';
|
|
||||||
import bridge from '../services/bridge';
|
|
||||||
|
|
||||||
export const declaration: CommandDeclaration = {
|
|
||||||
name: 'convertNoteToMarkdown',
|
|
||||||
label: () => _('Convert note to Markdown'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const runtime = (): CommandRuntime => {
|
|
||||||
return {
|
|
||||||
execute: async (context: CommandContext, noteId: string = null) => {
|
|
||||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
|
||||||
|
|
||||||
const note = await Note.load(noteId);
|
|
||||||
|
|
||||||
if (!note) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const markdownBody = await convertHtmlToMarkdown().execute(context, note.body);
|
|
||||||
|
|
||||||
const newNote = await Note.duplicate(note.id);
|
|
||||||
|
|
||||||
newNote.body = markdownBody;
|
|
||||||
newNote.markup_language = MarkupLanguage.Markdown;
|
|
||||||
|
|
||||||
await Note.save(newNote);
|
|
||||||
|
|
||||||
await Note.delete(note.id, { toTrash: true });
|
|
||||||
|
|
||||||
context.dispatch({
|
|
||||||
type: 'NOTE_HTML_TO_MARKDOWN_DONE',
|
|
||||||
value: note.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
context.dispatch({
|
|
||||||
type: 'NOTE_SELECT',
|
|
||||||
id: newNote.id,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
bridge().showErrorMessageBox(_('Could not convert note to Markdown: %s', error.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
enabledCondition: 'oneNoteSelected && noteIsHtml && !noteIsReadOnly',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||||
import * as convertNoteToMarkdown from './convertNoteToMarkdown';
|
|
||||||
import * as copyDevCommand from './copyDevCommand';
|
import * as copyDevCommand from './copyDevCommand';
|
||||||
import * as copyToClipboard from './copyToClipboard';
|
import * as copyToClipboard from './copyToClipboard';
|
||||||
import * as editProfileConfig from './editProfileConfig';
|
import * as editProfileConfig from './editProfileConfig';
|
||||||
@@ -14,6 +13,7 @@ import * as openProfileDirectory from './openProfileDirectory';
|
|||||||
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
|
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
|
||||||
import * as replaceMisspelling from './replaceMisspelling';
|
import * as replaceMisspelling from './replaceMisspelling';
|
||||||
import * as restoreNoteRevision from './restoreNoteRevision';
|
import * as restoreNoteRevision from './restoreNoteRevision';
|
||||||
|
import * as showProfileEditor from './showProfileEditor';
|
||||||
import * as startExternalEditing from './startExternalEditing';
|
import * as startExternalEditing from './startExternalEditing';
|
||||||
import * as stopExternalEditing from './stopExternalEditing';
|
import * as stopExternalEditing from './stopExternalEditing';
|
||||||
import * as switchProfile from './switchProfile';
|
import * as switchProfile from './switchProfile';
|
||||||
@@ -25,7 +25,6 @@ import * as toggleSafeMode from './toggleSafeMode';
|
|||||||
import * as toggleTabMovesFocus from './toggleTabMovesFocus';
|
import * as toggleTabMovesFocus from './toggleTabMovesFocus';
|
||||||
|
|
||||||
const index: any[] = [
|
const index: any[] = [
|
||||||
convertNoteToMarkdown,
|
|
||||||
copyDevCommand,
|
copyDevCommand,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
editProfileConfig,
|
editProfileConfig,
|
||||||
@@ -40,6 +39,7 @@ const index: any[] = [
|
|||||||
openSecondaryAppInstance,
|
openSecondaryAppInstance,
|
||||||
replaceMisspelling,
|
replaceMisspelling,
|
||||||
restoreNoteRevision,
|
restoreNoteRevision,
|
||||||
|
showProfileEditor,
|
||||||
startExternalEditing,
|
startExternalEditing,
|
||||||
stopExternalEditing,
|
stopExternalEditing,
|
||||||
switchProfile,
|
switchProfile,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { stateUtils } from '@joplin/lib/reducer';
|
import { stateUtils } from '@joplin/lib/reducer';
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import { AppState, createAppDefaultWindowState } from '../app.reducer';
|
import { createAppDefaultWindowState } from '../app.reducer';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
export const declaration: CommandDeclaration = {
|
export const declaration: CommandDeclaration = {
|
||||||
@@ -25,7 +25,7 @@ export const runtime = (): CommandRuntime => {
|
|||||||
folderId: note.parent_id,
|
folderId: note.parent_id,
|
||||||
windowId: `window-${noteId}-${idCounter++}`,
|
windowId: `window-${noteId}-${idCounter++}`,
|
||||||
defaultAppWindowState: {
|
defaultAppWindowState: {
|
||||||
...createAppDefaultWindowState(context.state as AppState),
|
...createAppDefaultWindowState(),
|
||||||
noteVisiblePanes: Setting.value('noteVisiblePanes'),
|
noteVisiblePanes: Setting.value('noteVisiblePanes'),
|
||||||
editorCodeView: Setting.value('editor.codeView'),
|
editorCodeView: Setting.value('editor.codeView'),
|
||||||
},
|
},
|
||||||
|
|||||||
20
packages/app-desktop/commands/showProfileEditor.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'showProfileEditor',
|
||||||
|
label: () => _('Manage profiles'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (context: CommandContext) => {
|
||||||
|
context.dispatch({
|
||||||
|
type: 'NAV_GO',
|
||||||
|
routeName: 'ProfileEditor',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enabledCondition: 'hasMultiProfiles',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@@ -261,7 +261,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServerSaml')) {
|
if (settings['sync.target'] === SyncTargetRegistry.nameToId('joplinServerSaml')) {
|
||||||
const server = settings['sync.11.path'] as string;
|
const server = settings['sync.11.path'] as string;
|
||||||
|
|
||||||
const goToSamlLogin = () => {
|
const goToSamlLogin = async () => {
|
||||||
|
// Save settings to allow SAML auth with the correct URL.
|
||||||
|
await shared.saveSettings(this);
|
||||||
|
|
||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: 'NAV_GO',
|
type: 'NAV_GO',
|
||||||
routeName: 'JoplinServerSamlLogin',
|
routeName: 'JoplinServerSamlLogin',
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { useContext, useEffect } from 'react';
|
|
||||||
import { _ } from '@joplin/lib/locale';
|
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
import { PopupNotificationContext } from '../PopupNotification/PopupNotificationProvider';
|
|
||||||
import { NotificationType } from '../PopupNotification/types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
noteId: string;
|
|
||||||
dispatch: Dispatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (props: Props) => {
|
|
||||||
const popupManager = useContext(PopupNotificationContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!props.noteId || props.noteId === '') return;
|
|
||||||
|
|
||||||
props.dispatch({ type: 'NOTE_HTML_TO_MARKDOWN_DONE', value: '' });
|
|
||||||
|
|
||||||
const notification = popupManager.createPopup(() => (
|
|
||||||
<div>{_('The note has been converted to Markdown and the original note has been moved to the trash')}</div>
|
|
||||||
), { type: NotificationType.Success });
|
|
||||||
notification.scheduleDismiss();
|
|
||||||
}, [props.dispatch, popupManager, props.noteId]);
|
|
||||||
|
|
||||||
return <div style={{ display: 'none' }}/>;
|
|
||||||
};
|
|
||||||
@@ -7,6 +7,7 @@ import useKeyboardHandler from './DialogButtonRow/useKeyboardHandler';
|
|||||||
export interface ButtonSpec {
|
export interface ButtonSpec {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClickEvent {
|
export interface ClickEvent {
|
||||||
@@ -51,21 +52,29 @@ export default function DialogButtonRow(props: Props) {
|
|||||||
if (props.onClick) props.onClick(event);
|
if (props.onClick) props.onClick(event);
|
||||||
}, [props.onClick]);
|
}, [props.onClick]);
|
||||||
|
|
||||||
const onKeyDown = useKeyboardHandler({ onOkButtonClick, onCancelButtonClick });
|
const okButtonShow = props.okButtonShow ?? true;
|
||||||
|
const cancelButtonShow = props.cancelButtonShow ?? true;
|
||||||
|
const canClickOk = okButtonShow && !props.okButtonDisabled;
|
||||||
|
const canClickCancel = cancelButtonShow && !props.cancelButtonDisabled;
|
||||||
|
|
||||||
|
const onKeyDown = useKeyboardHandler({
|
||||||
|
onOkButtonClick: canClickOk ? onOkButtonClick : null,
|
||||||
|
onCancelButtonClick: canClickCancel ? onCancelButtonClick : null,
|
||||||
|
});
|
||||||
|
|
||||||
const buttonComps = [];
|
const buttonComps = [];
|
||||||
|
|
||||||
if (props.customButtons) {
|
if (props.customButtons) {
|
||||||
for (const b of props.customButtons) {
|
for (const b of props.customButtons) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button key={b.name} style={buttonStyle} onClick={() => onCustomButtonClick({ buttonName: b.name })} onKeyDown={onKeyDown}>
|
<button key={b.name} style={buttonStyle} onClick={() => onCustomButtonClick({ buttonName: b.name })} disabled={b.disabled} onKeyDown={onKeyDown}>
|
||||||
{b.label}
|
{b.label}
|
||||||
</button>,
|
</button>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.okButtonShow !== false) {
|
if (okButtonShow) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={onOkButtonClick} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
<button disabled={props.okButtonDisabled} key="ok" style={buttonStyle} onClick={onOkButtonClick} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||||
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
|
{props.okButtonLabel ? props.okButtonLabel : _('OK')}
|
||||||
@@ -73,7 +82,7 @@ export default function DialogButtonRow(props: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.cancelButtonShow !== false) {
|
if (cancelButtonShow) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button disabled={props.cancelButtonDisabled} key="cancel" style={{ ...buttonStyle }} onClick={onCancelButtonClick}>
|
<button disabled={props.cancelButtonDisabled} key="cancel" style={{ ...buttonStyle }} onClick={onCancelButtonClick}>
|
||||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import * as React from 'react';
|
|||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
import { isInsideContainer } from '@joplin/lib/dom';
|
import { isInsideContainer } from '@joplin/lib/dom';
|
||||||
|
|
||||||
|
type OnButtonClick = ()=> void;
|
||||||
interface Props {
|
interface Props {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
onOkButtonClick: null|OnButtonClick;
|
||||||
onOkButtonClick: Function;
|
onCancelButtonClick: null|OnButtonClick;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
|
||||||
onCancelButtonClick: Function;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalKeydownHandlers: string[] = [];
|
const globalKeydownHandlers: string[] = [];
|
||||||
@@ -48,15 +47,17 @@ export default (props: Props) => {
|
|||||||
|
|
||||||
if (!isTopDialog() || isInSubModal(event.target)) return;
|
if (!isTopDialog() || isInSubModal(event.target)) return;
|
||||||
|
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13 && props.onOkButtonClick) {
|
||||||
if ('nodeName' in event.target && event.target.nodeName === 'INPUT') {
|
if ('nodeName' in event.target && event.target.nodeName === 'INPUT') {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
|
|
||||||
if (target.type !== 'button' && target.type !== 'checkbox') {
|
if (target.type !== 'button' && target.type !== 'checkbox') {
|
||||||
|
event.preventDefault();
|
||||||
props.onOkButtonClick();
|
props.onOkButtonClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.keyCode === 27) {
|
} else if (event.keyCode === 27 && props.onCancelButtonClick) {
|
||||||
|
event.preventDefault();
|
||||||
props.onCancelButtonClick();
|
props.onCancelButtonClick();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
|
|||||||
<span className={state.className}>{state.errorMessage}</span>
|
<span className={state.className}>{state.errorMessage}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
{state.active === 'LINK_USED' ? <div id="loading-animation" /> : null}
|
{state.active === 'LINK_USED' ? <div className="loading-animation" /> : null}
|
||||||
<JoplinCloudSignUpCallToAction />
|
<JoplinCloudSignUpCallToAction />
|
||||||
</div>
|
</div>
|
||||||
<ButtonBar onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })} />
|
<ButtonBar onCancelClick={() => props.dispatch({ type: 'NAV_BACK' })} />
|
||||||
|
|||||||
@@ -38,14 +38,12 @@ import restart from '../services/restart';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
|
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
|
||||||
import validateColumns from './NoteListHeader/utils/validateColumns';
|
import validateColumns from './NoteListHeader/utils/validateColumns';
|
||||||
import ConversionNotification from './ConversionNotification/ConversionNotification';
|
|
||||||
import TrashNotification from './TrashNotification/TrashNotification';
|
import TrashNotification from './TrashNotification/TrashNotification';
|
||||||
import UpdateNotification from './UpdateNotification/UpdateNotification';
|
import UpdateNotification from './UpdateNotification/UpdateNotification';
|
||||||
import NoteEditor from './NoteEditor/NoteEditor';
|
import NoteEditor from './NoteEditor/NoteEditor';
|
||||||
import PluginNotification from './PluginNotification/PluginNotification';
|
import PluginNotification from './PluginNotification/PluginNotification';
|
||||||
import { Toast } from '@joplin/lib/services/plugins/api/types';
|
import { Toast } from '@joplin/lib/services/plugins/api/types';
|
||||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
|
|
||||||
const ipcRenderer = require('electron').ipcRenderer;
|
const ipcRenderer = require('electron').ipcRenderer;
|
||||||
|
|
||||||
@@ -86,7 +84,6 @@ interface Props {
|
|||||||
showInvalidJoplinCloudCredential: boolean;
|
showInvalidJoplinCloudCredential: boolean;
|
||||||
toast: Toast;
|
toast: Toast;
|
||||||
shouldSwitchToAppleSiliconVersion: boolean;
|
shouldSwitchToAppleSiliconVersion: boolean;
|
||||||
noteHtmlToMarkdownDone: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShareFolderDialogOptions {
|
interface ShareFolderDialogOptions {
|
||||||
@@ -800,10 +797,6 @@ class MainScreenComponent extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
<ConversionNotification
|
|
||||||
noteId={this.props.noteHtmlToMarkdownDone}
|
|
||||||
dispatch={this.props.dispatch as Dispatch}
|
|
||||||
/>
|
|
||||||
<TrashNotification
|
<TrashNotification
|
||||||
lastDeletion={this.props.lastDeletion}
|
lastDeletion={this.props.lastDeletion}
|
||||||
lastDeletionNotificationTime={this.props.lastDeletionNotificationTime}
|
lastDeletionNotificationTime={this.props.lastDeletionNotificationTime}
|
||||||
@@ -859,8 +852,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
notesColumns: validateColumns(state.settings['notes.columns']),
|
notesColumns: validateColumns(state.settings['notes.columns']),
|
||||||
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
|
||||||
toast: state.toast,
|
toast: state.toast,
|
||||||
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
|
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && shim.isMac() && process.arch !== 'arm64',
|
||||||
noteHtmlToMarkdownDone: state.noteHtmlToMarkdownDone,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins
|
|||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
|
import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
|
||||||
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
|
|
||||||
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
||||||
import InteropServiceHelper from '../InteropServiceHelper';
|
import InteropServiceHelper from '../InteropServiceHelper';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
@@ -29,6 +28,8 @@ import { EventName } from '@joplin/lib/eventManager';
|
|||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import NavService from '@joplin/lib/services/NavService';
|
import NavService from '@joplin/lib/services/NavService';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import { ImportCommandOptions } from './WindowCommandsAndDialogs/commands/importFrom';
|
||||||
|
import { FileSystemItem } from '@joplin/lib/services/interop/types';
|
||||||
|
|
||||||
const logger = Logger.create('MenuBar');
|
const logger = Logger.create('MenuBar');
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: an
|
|||||||
|
|
||||||
switchProfileMenuItems.push({ type: 'separator' });
|
switchProfileMenuItems.push({ type: 'separator' });
|
||||||
switchProfileMenuItems.push(menuItemDic.addProfile);
|
switchProfileMenuItems.push(menuItemDic.addProfile);
|
||||||
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
|
switchProfileMenuItems.push(menuItemDic.showProfileEditor);
|
||||||
|
|
||||||
return switchProfileMenuItems;
|
return switchProfileMenuItems;
|
||||||
}, [profileConfig, menuItemDic]);
|
}, [profileConfig, menuItemDic]);
|
||||||
@@ -304,83 +305,16 @@ function useMenu(props: Props) {
|
|||||||
void CommandService.instance().execute(commandName);
|
void CommandService.instance().execute(commandName);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: string) => {
|
const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: FileSystemItem) => {
|
||||||
let path = null;
|
const options: ImportCommandOptions = {
|
||||||
|
|
||||||
if (moduleSource === 'file') {
|
|
||||||
path = await bridge().showOpenDialog({
|
|
||||||
filters: [{ name: module.description, extensions: module.fileExtensions }],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
path = await bridge().showOpenDialog({
|
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path || (Array.isArray(path) && !path.length)) return;
|
|
||||||
|
|
||||||
if (Array.isArray(path)) path = path[0];
|
|
||||||
|
|
||||||
const modalMessage = _('Importing from "%s" as "%s" format. Please wait...', path, module.format);
|
|
||||||
|
|
||||||
void CommandService.instance().execute('showModalMessage', modalMessage);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const errors: any[] = [];
|
|
||||||
|
|
||||||
const importOptions = {
|
|
||||||
path,
|
|
||||||
format: module.format,
|
|
||||||
outputFormat: module.outputFormat,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onProgress: (status: any) => {
|
|
||||||
const statusStrings: string[] = Object.keys(status).map((key: string) => {
|
|
||||||
return `${key}: ${status[key]}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onError: (error: any) => {
|
|
||||||
errors.push(error);
|
|
||||||
console.warn(error);
|
|
||||||
},
|
|
||||||
destinationFolderId: !module.isNoteArchive && moduleSource === 'file' ? props.selectedFolderId : null,
|
destinationFolderId: !module.isNoteArchive && moduleSource === 'file' ? props.selectedFolderId : null,
|
||||||
|
sourcePath: undefined, // Show a file picker
|
||||||
|
sourceType: moduleSource,
|
||||||
|
importFormat: module.format,
|
||||||
|
outputFormat: module.outputFormat,
|
||||||
};
|
};
|
||||||
|
await CommandService.instance().execute('importFrom', options);
|
||||||
const service = InteropService.instance();
|
}, [props.selectedFolderId]);
|
||||||
try {
|
|
||||||
const result = await service.import(importOptions);
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.info('Import result: ', result);
|
|
||||||
} catch (error) {
|
|
||||||
bridge().showErrorMessageBox(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CommandService.instance().execute('hideModalMessage');
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
const response = bridge().showErrorMessageBox('There was some errors importing the notes - check the console for more details.\n\nPlease consider sending a bug report to the forum!', {
|
|
||||||
buttons: [_('Close'), _('Send bug report')],
|
|
||||||
});
|
|
||||||
|
|
||||||
props.dispatch({ type: 'NOTE_DEVTOOLS_SET', value: true });
|
|
||||||
|
|
||||||
if (response === 1) {
|
|
||||||
const url = makeDiscourseDebugUrl(
|
|
||||||
`Error importing notes from format: ${module.format}`,
|
|
||||||
`- Input format: ${module.format}\n- Output format: ${module.outputFormat}`,
|
|
||||||
errors,
|
|
||||||
packageInfo,
|
|
||||||
PluginService.instance(),
|
|
||||||
props.pluginSettings,
|
|
||||||
);
|
|
||||||
|
|
||||||
void bridge().openExternal(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
|
||||||
}, [props.selectedFolderId, props.pluginSettings]);
|
|
||||||
|
|
||||||
const onMenuItemClickRef = useRef(null);
|
const onMenuItemClickRef = useRef(null);
|
||||||
onMenuItemClickRef.current = onMenuItemClick;
|
onMenuItemClickRef.current = onMenuItemClick;
|
||||||
|
|||||||
@@ -22,12 +22,6 @@ interface MultiNoteActionsProps {
|
|||||||
function styles_(props: MultiNoteActionsProps) {
|
function styles_(props: MultiNoteActionsProps) {
|
||||||
return buildStyle('MultiNoteActions', props.themeId, (theme: ThemeStyle) => {
|
return buildStyle('MultiNoteActions', props.themeId, (theme: ThemeStyle) => {
|
||||||
return {
|
return {
|
||||||
root: {
|
|
||||||
display: 'inline-flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingTop: theme.marginTop,
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
itemList: {
|
itemList: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -90,7 +84,7 @@ export default function MultiNoteActions(props: MultiNoteActionsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.root}>
|
<div style={styles.root} className='multi-note-actions'>
|
||||||
<div style={styles.itemList}>{itemComps}</div>
|
<div style={styles.itemList}>{itemComps}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,13 +31,15 @@ function markupToHtml() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, lineSetter: Function) {
|
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: Function) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
Countable.count(text, (counter: any) => {
|
Countable.count(text, (counter: any) => {
|
||||||
wordSetter(counter.words);
|
wordSetter(counter.words);
|
||||||
characterSetter(counter.all);
|
characterSetter(counter.all);
|
||||||
characterNoSpaceSetter(counter.characters);
|
characterNoSpaceSetter(counter.characters);
|
||||||
});
|
});
|
||||||
|
const cjkMatches = text.match(/[\p{Script=Han}\p{Script=Bopomofo}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu);
|
||||||
|
cjkCharacterSetter(cjkMatches ? cjkMatches.length : 0);
|
||||||
lineSetter(text === '' ? 0 : text.split('\n').length);
|
lineSetter(text === '' ? 0 : text.split('\n').length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,23 +60,25 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
|||||||
const [words, setWords] = useState<number>(0);
|
const [words, setWords] = useState<number>(0);
|
||||||
const [characters, setCharacters] = useState<number>(0);
|
const [characters, setCharacters] = useState<number>(0);
|
||||||
const [charactersNoSpace, setCharactersNoSpace] = useState<number>(0);
|
const [charactersNoSpace, setCharactersNoSpace] = useState<number>(0);
|
||||||
|
const [cjkCharacters, setCjkCharacters] = useState<number>(0);
|
||||||
// For source with Markdown syntax stripped out
|
// For source with Markdown syntax stripped out
|
||||||
const [strippedLines, setStrippedLines] = useState<number>(0);
|
const [strippedLines, setStrippedLines] = useState<number>(0);
|
||||||
const [strippedWords, setStrippedWords] = useState<number>(0);
|
const [strippedWords, setStrippedWords] = useState<number>(0);
|
||||||
const [strippedCharacters, setStrippedCharacters] = useState<number>(0);
|
const [strippedCharacters, setStrippedCharacters] = useState<number>(0);
|
||||||
const [strippedCharactersNoSpace, setStrippedCharactersNoSpace] = useState<number>(0);
|
const [strippedCharactersNoSpace, setStrippedCharactersNoSpace] = useState<number>(0);
|
||||||
|
const [strippedCjkCharacters, setStrippedCjkCharacters] = useState<number>(0);
|
||||||
const [strippedReadTime, setStrippedReadTime] = useState<number>(0);
|
const [strippedReadTime, setStrippedReadTime] = useState<number>(0);
|
||||||
// This amount based on the following paper:
|
// This amount based on the following paper:
|
||||||
// https://www.researchgate.net/publication/332380784_How_many_words_do_we_read_per_minute_A_review_and_meta-analysis_of_reading_rate
|
// https://www.researchgate.net/publication/332380784_How_many_words_do_we_read_per_minute_A_review_and_meta-analysis_of_reading_rate
|
||||||
const wordsPerMinute = 250;
|
const wordsPerMinute = 250;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setLines);
|
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setCjkCharacters, setLines);
|
||||||
}, [props.text]);
|
}, [props.text]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
|
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
|
||||||
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
|
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedCjkCharacters, setStrippedLines);
|
||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||||
}, [props.text]);
|
}, [props.text]);
|
||||||
|
|
||||||
@@ -88,6 +92,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
|||||||
words: words,
|
words: words,
|
||||||
characters: characters,
|
characters: characters,
|
||||||
charactersNoSpace: charactersNoSpace,
|
charactersNoSpace: charactersNoSpace,
|
||||||
|
cjkCharacters: cjkCharacters,
|
||||||
};
|
};
|
||||||
|
|
||||||
const strippedTextProperties: TextPropertiesMap = {
|
const strippedTextProperties: TextPropertiesMap = {
|
||||||
@@ -99,12 +104,14 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
|||||||
words: strippedWords,
|
words: strippedWords,
|
||||||
characters: strippedCharacters,
|
characters: strippedCharacters,
|
||||||
charactersNoSpace: strippedCharactersNoSpace,
|
charactersNoSpace: strippedCharactersNoSpace,
|
||||||
|
cjkCharacters: strippedCjkCharacters,
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyToLabel: KeyToLabelMap = {
|
const keyToLabel: KeyToLabelMap = {
|
||||||
words: _('Words'),
|
words: _('Words'),
|
||||||
characters: _('Characters'),
|
characters: _('Characters'),
|
||||||
charactersNoSpace: _('Characters excluding spaces'),
|
charactersNoSpace: _('Characters excluding spaces'),
|
||||||
|
cjkCharacters: _('Chinese/Japanese/Korean characters'),
|
||||||
lines: _('Lines'),
|
lines: _('Lines'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,6 +154,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const key in textProperties) {
|
for (const key in textProperties) {
|
||||||
|
if (key === 'cjkCharacters' && textProperties[key] === 0 && strippedTextProperties[key] === 0) continue;
|
||||||
const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
|
const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
|
||||||
tableBodyComps.push(comp);
|
tableBodyComps.push(comp);
|
||||||
}
|
}
|
||||||
@@ -172,7 +180,12 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
|||||||
<div style={{ ...labelCompStyle, marginTop: 10 }}>
|
<div style={{ ...labelCompStyle, marginTop: 10 }}>
|
||||||
{readTimeLabel}
|
{readTimeLabel}
|
||||||
</div>
|
</div>
|
||||||
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
|
<DialogButtonRow
|
||||||
|
themeId={props.themeId}
|
||||||
|
onClick={buttonRow_click}
|
||||||
|
okButtonShow={false}
|
||||||
|
cancelButtonLabel={_('Close')}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,36 @@ const logger = Logger.create('useEditorSearch');
|
|||||||
// Registers a helper CodeMirror extension to be used with
|
// Registers a helper CodeMirror extension to be used with
|
||||||
// useEditorSearchHandler.
|
// useEditorSearchHandler.
|
||||||
|
|
||||||
export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulation) {
|
interface SetMarkersOptions {
|
||||||
|
selectedIndex: number;
|
||||||
|
searchTimestamp: number;
|
||||||
|
showEditorMarkers?: boolean;
|
||||||
|
withSelection?: boolean;
|
||||||
|
}
|
||||||
|
type Keyword = { value: string };
|
||||||
|
|
||||||
|
export type OnSetMarkers = (cm: CodeMirror5Emulation, keywords: Keyword[], options: SetMarkersOptions)=> number;
|
||||||
|
|
||||||
|
|
||||||
|
// Modified from codemirror/addons/search/search.js
|
||||||
|
const searchOverlay = (query: RegExp) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
return { token: function(stream: any) {
|
||||||
|
query.lastIndex = stream.pos;
|
||||||
|
const match = query.exec(stream.string);
|
||||||
|
if (match && match.index === stream.pos) {
|
||||||
|
stream.pos += match[0].length || 1;
|
||||||
|
return 'search-marker';
|
||||||
|
} else if (match) {
|
||||||
|
stream.pos = match.index;
|
||||||
|
} else {
|
||||||
|
stream.skipToEnd();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useEditorSearchExtension() {
|
||||||
|
|
||||||
const [markers, setMarkers] = useState([]);
|
const [markers, setMarkers] = useState([]);
|
||||||
const [overlay, setOverlay] = useState(null);
|
const [overlay, setOverlay] = useState(null);
|
||||||
@@ -48,23 +77,6 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
|||||||
setOverlayTimeout(null);
|
setOverlayTimeout(null);
|
||||||
}, [scrollbarMarks, overlay, overlayTimeout]);
|
}, [scrollbarMarks, overlay, overlayTimeout]);
|
||||||
|
|
||||||
// Modified from codemirror/addons/search/search.js
|
|
||||||
const searchOverlay = useCallback((query: RegExp) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
return { token: function(stream: any) {
|
|
||||||
query.lastIndex = stream.pos;
|
|
||||||
const match = query.exec(stream.string);
|
|
||||||
if (match && match.index === stream.pos) {
|
|
||||||
stream.pos += match[0].length || 1;
|
|
||||||
return 'search-marker';
|
|
||||||
} else if (match) {
|
|
||||||
stream.pos = match.index;
|
|
||||||
} else {
|
|
||||||
stream.skipToEnd();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Highlights the currently active found work
|
// Highlights the currently active found work
|
||||||
// It's possible to get tricky with this functions and just use findNext/findPrev
|
// It's possible to get tricky with this functions and just use findNext/findPrev
|
||||||
@@ -115,16 +127,17 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const onSetMarkers: OnSetMarkers = (cm, keywords, options) => {
|
||||||
CodeMirror?.defineExtension('setMarkers', function(keywords: any, options: any) {
|
// Pass arguments in via options to allow the extension to work if multiple editors are open simultaneously
|
||||||
|
// See https://github.com/laurent22/joplin/issues/13399.
|
||||||
if (!options) {
|
if (!options) {
|
||||||
options = { selectedIndex: 0, searchTimestamp: 0 };
|
options = { selectedIndex: 0, searchTimestamp: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.showEditorMarkers === false) {
|
if (options.showEditorMarkers === false) {
|
||||||
clearMarkers();
|
clearMarkers();
|
||||||
clearOverlay(this);
|
clearOverlay(cm);
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMarkers();
|
clearMarkers();
|
||||||
@@ -145,7 +158,7 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
|||||||
const scrollTo = i === 0 && (previousKeywordValue !== keyword.value || previousIndex !== options.selectedIndex || options.searchTimestamp !== previousSearchTimestamp);
|
const scrollTo = i === 0 && (previousKeywordValue !== keyword.value || previousIndex !== options.selectedIndex || options.searchTimestamp !== previousSearchTimestamp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const match = highlightSearch(this, searchTerm, options.selectedIndex, scrollTo, !!options.withSelection);
|
const match = highlightSearch(cm, searchTerm, options.selectedIndex, scrollTo, !!options.withSelection);
|
||||||
if (match) marks.push(match);
|
if (match) marks.push(match);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'SyntaxError') {
|
if (error.name !== 'SyntaxError') {
|
||||||
@@ -165,7 +178,7 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
|||||||
// SEARCHOVERLAY
|
// SEARCHOVERLAY
|
||||||
// We only want to highlight all matches when there is only 1 search term
|
// We only want to highlight all matches when there is only 1 search term
|
||||||
if (keywords.length !== 1 || keywords[0].value === '') {
|
if (keywords.length !== 1 || keywords[0].value === '') {
|
||||||
clearOverlay(this);
|
clearOverlay(cm);
|
||||||
const prev = keywords.length > 1 ? keywords[0].value : '';
|
const prev = keywords.length > 1 ? keywords[0].value : '';
|
||||||
setPreviousKeywordValue(prev);
|
setPreviousKeywordValue(prev);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -175,22 +188,22 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
|||||||
|
|
||||||
// Determine the number of matches in the source, this is passed on
|
// Determine the number of matches in the source, this is passed on
|
||||||
// to the NoteEditor component
|
// to the NoteEditor component
|
||||||
const regexMatches = this.getValue().match(searchTerm);
|
const regexMatches = cm.getValue().match(searchTerm);
|
||||||
const nMatches = regexMatches ? regexMatches.length : 0;
|
const nMatches = regexMatches ? regexMatches.length : 0;
|
||||||
|
|
||||||
// Don't bother clearing and re-calculating the overlay if the search term
|
// Don't bother clearing and re-calculating the overlay if the search term
|
||||||
// hasn't changed
|
// hasn't changed
|
||||||
if (keywords[0].value === previousKeywordValue) return nMatches;
|
if (keywords[0].value === previousKeywordValue) return nMatches;
|
||||||
|
|
||||||
clearOverlay(this);
|
clearOverlay(cm);
|
||||||
setPreviousKeywordValue(keywords[0].value);
|
setPreviousKeywordValue(keywords[0].value);
|
||||||
|
|
||||||
// These operations are pretty slow, so we won't add use them until the user
|
// These operations are pretty slow, so we won't add use them until the user
|
||||||
// has finished typing, 500ms is probably enough time
|
// has finished typing, 500ms is probably enough time
|
||||||
const timeout = shim.setTimeout(() => {
|
const timeout = shim.setTimeout(() => {
|
||||||
const scrollMarks = this.showMatchesOnScrollbar?.(searchTerm, true, 'cm-search-marker-scrollbar');
|
const scrollMarks = cm.showMatchesOnScrollbar?.(searchTerm, true, 'cm-search-marker-scrollbar');
|
||||||
const overlay = searchOverlay(searchTerm);
|
const overlay = searchOverlay(searchTerm);
|
||||||
this.addOverlay(overlay);
|
cm.addOverlay(overlay);
|
||||||
setOverlay(overlay);
|
setOverlay(overlay);
|
||||||
setScrollbarMarks(scrollMarks);
|
setScrollbarMarks(scrollMarks);
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -199,5 +212,9 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
|
|||||||
overlayTimeoutRef.current = timeout;
|
overlayTimeoutRef.current = timeout;
|
||||||
|
|
||||||
return nMatches;
|
return nMatches;
|
||||||
});
|
};
|
||||||
|
const onSetMarkersRef = useRef(onSetMarkers);
|
||||||
|
onSetMarkersRef.current = onSetMarkers;
|
||||||
|
|
||||||
|
return { onSetMarkersRef };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { RefObject, useEffect, useMemo, useRef } from 'react';
|
|||||||
import usePrevious from '../../../../hooks/usePrevious';
|
import usePrevious from '../../../../hooks/usePrevious';
|
||||||
import { RenderedBody } from './types';
|
import { RenderedBody } from './types';
|
||||||
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
||||||
|
import CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation';
|
||||||
|
import useEditorSearchExtension from './useEditorSearchExtension';
|
||||||
const debounce = require('debounce');
|
const debounce = require('debounce');
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -10,8 +12,7 @@ interface Props {
|
|||||||
searchMarkers: any;
|
searchMarkers: any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
webviewRef: RefObject<any>;
|
webviewRef: RefObject<any>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
editorRef: RefObject<CodeMirror5Emulation>;
|
||||||
editorRef: RefObject<any>;
|
|
||||||
|
|
||||||
noteContent: string;
|
noteContent: string;
|
||||||
renderedBody: RenderedBody;
|
renderedBody: RenderedBody;
|
||||||
@@ -23,6 +24,8 @@ const useEditorSearchHandler = (props: Props) => {
|
|||||||
webviewRef, editorRef, renderedBody, noteContent, searchMarkers, showEditorMarkers,
|
webviewRef, editorRef, renderedBody, noteContent, searchMarkers, showEditorMarkers,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const { onSetMarkersRef } = useEditorSearchExtension();
|
||||||
|
|
||||||
const previousContent = usePrevious(noteContent);
|
const previousContent = usePrevious(noteContent);
|
||||||
const previousRenderedBody = usePrevious(renderedBody);
|
const previousRenderedBody = usePrevious(renderedBody);
|
||||||
const previousSearchMarkers = usePrevious(searchMarkers);
|
const previousSearchMarkers = usePrevious(searchMarkers);
|
||||||
@@ -31,15 +34,15 @@ const useEditorSearchHandler = (props: Props) => {
|
|||||||
|
|
||||||
// Fixes https://github.com/laurent22/joplin/issues/7565
|
// Fixes https://github.com/laurent22/joplin/issues/7565
|
||||||
const debouncedMarkers = useMemo(() => debounce((searchMarkers: SearchMarkers) => {
|
const debouncedMarkers = useMemo(() => debounce((searchMarkers: SearchMarkers) => {
|
||||||
if (!editorRef.current) return;
|
if (!onSetMarkersRef.current) return;
|
||||||
|
|
||||||
if (showEditorMarkersRef.current) {
|
if (showEditorMarkersRef.current) {
|
||||||
const matches = editorRef.current.setMarkers(searchMarkers.keywords, searchMarkers.options);
|
const matches = onSetMarkersRef.current(editorRef.current, searchMarkers.keywords, searchMarkers.options);
|
||||||
props.setLocalSearchResultCount(matches);
|
props.setLocalSearchResultCount(matches);
|
||||||
} else {
|
} else {
|
||||||
editorRef.current.setMarkers(searchMarkers.keywords, { ...searchMarkers.options, showEditorMarkers: false });
|
onSetMarkersRef.current(editorRef.current, searchMarkers.keywords, { ...searchMarkers.options, showEditorMarkers: false });
|
||||||
}
|
}
|
||||||
}, 50), [editorRef, props.setLocalSearchResultCount]);
|
}, 50), [editorRef, onSetMarkersRef, props.setLocalSearchResultCount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchMarkers) return () => {};
|
if (!searchMarkers) return () => {};
|
||||||
@@ -59,7 +62,7 @@ const useEditorSearchHandler = (props: Props) => {
|
|||||||
}
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [
|
}, [
|
||||||
editorRef,
|
onSetMarkersRef,
|
||||||
webviewRef,
|
webviewRef,
|
||||||
searchMarkers,
|
searchMarkers,
|
||||||
previousSearchMarkers,
|
previousSearchMarkers,
|
||||||
@@ -71,6 +74,10 @@ const useEditorSearchHandler = (props: Props) => {
|
|||||||
debouncedMarkers,
|
debouncedMarkers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Returned to allow quickly setting the initial search markers just after the editor loads.
|
||||||
|
onSetInitialMarkersRef: onSetMarkersRef,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useEditorSearchHandler;
|
export default useEditorSearchHandler;
|
||||||
|
|||||||
@@ -695,7 +695,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||||
}, [renderedBody, webviewReady]);
|
}, [renderedBody, webviewReady]);
|
||||||
|
|
||||||
useEditorSearchHandler({
|
const { onSetInitialMarkersRef } = useEditorSearchHandler({
|
||||||
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
||||||
searchMarkers: props.searchMarkers,
|
searchMarkers: props.searchMarkers,
|
||||||
webviewRef,
|
webviewRef,
|
||||||
@@ -737,6 +737,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
<Editor
|
<Editor
|
||||||
value={props.content}
|
value={props.content}
|
||||||
searchMarkers={props.searchMarkers}
|
searchMarkers={props.searchMarkers}
|
||||||
|
onSetMarkersRef={onSetInitialMarkersRef}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
mode={props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'}
|
mode={props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML ? 'xml' : 'joplin-markdown'}
|
||||||
codeMirrorTheme={styles.editor.codeMirrorTheme}
|
codeMirrorTheme={styles.editor.codeMirrorTheme}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef } from 'react';
|
import { useEffect, useImperativeHandle, useState, useRef, useCallback, forwardRef, RefObject } from 'react';
|
||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
|
||||||
import CodeMirror from 'codemirror';
|
import CodeMirror from 'codemirror';
|
||||||
@@ -16,7 +16,7 @@ import useListIdent from './utils/useListIdent';
|
|||||||
import useScrollUtils from './utils/useScrollUtils';
|
import useScrollUtils from './utils/useScrollUtils';
|
||||||
import useCursorUtils from './utils/useCursorUtils';
|
import useCursorUtils from './utils/useCursorUtils';
|
||||||
import useLineSorting from './utils/useLineSorting';
|
import useLineSorting from './utils/useLineSorting';
|
||||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
import { OnSetMarkers } from '../utils/useEditorSearchExtension';
|
||||||
import useJoplinMode from './utils/useJoplinMode';
|
import useJoplinMode from './utils/useJoplinMode';
|
||||||
import useKeymap from './utils/useKeymap';
|
import useKeymap from './utils/useKeymap';
|
||||||
import useExternalPlugins from './utils/useExternalPlugins';
|
import useExternalPlugins from './utils/useExternalPlugins';
|
||||||
@@ -77,6 +77,7 @@ export interface EditorProps {
|
|||||||
value: string;
|
value: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
searchMarkers: any;
|
searchMarkers: any;
|
||||||
|
onSetMarkersRef: RefObject<OnSetMarkers>;
|
||||||
mode: string;
|
mode: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
style: any;
|
style: any;
|
||||||
@@ -119,7 +120,6 @@ function Editor(props: EditorProps, ref: any) {
|
|||||||
useScrollUtils(CodeMirror);
|
useScrollUtils(CodeMirror);
|
||||||
useCursorUtils(CodeMirror);
|
useCursorUtils(CodeMirror);
|
||||||
useLineSorting(CodeMirror);
|
useLineSorting(CodeMirror);
|
||||||
useEditorSearch(CodeMirror);
|
|
||||||
useJoplinMode(CodeMirror);
|
useJoplinMode(CodeMirror);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const pluginOptions: any = useExternalPlugins(CodeMirror, props.plugins);
|
const pluginOptions: any = useExternalPlugins(CodeMirror, props.plugins);
|
||||||
@@ -228,7 +228,7 @@ function Editor(props: EditorProps, ref: any) {
|
|||||||
// It's possible for searchMarkers to be available before the editor
|
// It's possible for searchMarkers to be available before the editor
|
||||||
// In these cases we set the markers asap so the user can see them as
|
// In these cases we set the markers asap so the user can see them as
|
||||||
// soon as the editor is ready
|
// soon as the editor is ready
|
||||||
if (props.searchMarkers) { cm.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options); }
|
if (props.searchMarkers) { props.onSetMarkersRef.current(cm, props.searchMarkers.keywords, props.searchMarkers.options); }
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Clean up codemirror
|
// Clean up codemirror
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { _ } from '@joplin/lib/locale';
|
|||||||
import bridge from '../../../../../services/bridge';
|
import bridge from '../../../../../services/bridge';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import { MarkupToHtml } from '@joplin/renderer';
|
import { MarkupToHtml } from '@joplin/renderer';
|
||||||
const { clipboard } = require('electron');
|
import { clipboard } from 'electron';
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
import ErrorBoundary from '../../../../ErrorBoundary';
|
import ErrorBoundary from '../../../../ErrorBoundary';
|
||||||
import { EditorKeymap, EditorLanguageType, EditorSettings, SearchState, UserEventSource } from '@joplin/editor/types';
|
import { EditorKeymap, EditorLanguageType, EditorSettings, SearchState, UserEventSource } from '@joplin/editor/types';
|
||||||
@@ -32,6 +32,7 @@ import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange
|
|||||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||||
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
|
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
|
||||||
import useSyncEditorValue from './utils/useSyncEditorValue';
|
import useSyncEditorValue from './utils/useSyncEditorValue';
|
||||||
|
import { getGlobalSettings } from '@joplin/renderer/types';
|
||||||
|
|
||||||
const logger = Logger.create('CodeMirror6');
|
const logger = Logger.create('CodeMirror6');
|
||||||
const logDebug = (message: string) => logger.debug(message);
|
const logDebug = (message: string) => logger.debug(message);
|
||||||
@@ -93,41 +94,13 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
|
|
||||||
const editorCutText = useCallback(() => {
|
const editorCutText = useCallback(() => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
const selections = editorRef.current.getSelections();
|
editorRef.current.cutText(text => clipboard.writeText(text));
|
||||||
if (selections.length > 0 && selections[0]) {
|
|
||||||
clipboard.writeText(selections[0]);
|
|
||||||
// Easy way to wipe out just the first selection
|
|
||||||
selections[0] = '';
|
|
||||||
editorRef.current.replaceSelections(selections);
|
|
||||||
} else {
|
|
||||||
const cursor = editorRef.current.getCursor();
|
|
||||||
const line = editorRef.current.getLine(cursor.line);
|
|
||||||
clipboard.writeText(`${line}\n`);
|
|
||||||
const startLine = editorRef.current.getCursor('head');
|
|
||||||
startLine.ch = 0;
|
|
||||||
const endLine = {
|
|
||||||
line: startLine.line + 1,
|
|
||||||
ch: 0,
|
|
||||||
};
|
|
||||||
editorRef.current.replaceRange('', startLine, endLine);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const editorCopyText = useCallback(() => {
|
const editorCopyText = useCallback(() => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
const selections = editorRef.current.getSelections();
|
editorRef.current.copyText(text => clipboard.writeText(text));
|
||||||
|
|
||||||
// Handle the case when there is a selection - copy the selection to the clipboard
|
|
||||||
// When there is no selection, the selection array contains an empty string.
|
|
||||||
if (selections.length > 0 && selections[0]) {
|
|
||||||
clipboard.writeText(selections[0]);
|
|
||||||
} else {
|
|
||||||
// This is the case when there is no selection - copy the current line to the clipboard
|
|
||||||
const cursor = editorRef.current.getCursor();
|
|
||||||
const line = editorRef.current.getLine(cursor.line);
|
|
||||||
clipboard.writeText(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -248,6 +221,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
useCustomPdfViewer: props.useCustomPdfViewer,
|
useCustomPdfViewer: props.useCustomPdfViewer,
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
vendorDir: bridge().vendorDir(),
|
vendorDir: bridge().vendorDir(),
|
||||||
|
globalSettings: getGlobalSettings(Setting),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -392,6 +366,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
ignoreModifiers: true,
|
ignoreModifiers: true,
|
||||||
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
||||||
keymap: keyboardMode,
|
keymap: keyboardMode,
|
||||||
|
preferMacShortcuts: shim.isMac(),
|
||||||
indentWithTabs: true,
|
indentWithTabs: true,
|
||||||
tabMovesFocus: props.tabMovesFocus,
|
tabMovesFocus: props.tabMovesFocus,
|
||||||
editorLabel: _('Markdown editor'),
|
editorLabel: _('Markdown editor'),
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
|
|||||||
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import useKeymap from './utils/useKeymap';
|
import useKeymap from './utils/useKeymap';
|
||||||
import useEditorSearch from '../utils/useEditorSearchExtension';
|
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
||||||
import localisation from './utils/localisation';
|
import localisation from './utils/localisation';
|
||||||
@@ -44,8 +43,6 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
|||||||
onLogMessageRef.current = props.onLogMessage;
|
onLogMessageRef.current = props.onLogMessage;
|
||||||
}, [props.onEvent, props.onLogMessage]);
|
}, [props.onEvent, props.onLogMessage]);
|
||||||
|
|
||||||
useEditorSearch(editor);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return () => {};
|
return () => {};
|
||||||
|
|||||||
@@ -919,8 +919,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
|||||||
|
|
||||||
editor.on('SetContent', () => {
|
editor.on('SetContent', () => {
|
||||||
preprocessContent();
|
preprocessContent();
|
||||||
|
|
||||||
props_onMessage.current({ channel: 'noteRenderComplete' });
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1047,12 +1045,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onInitialContentSet } = useCursorPositioning({
|
const { onRestoreCursorPosition } = useCursorPositioning({
|
||||||
initialCursorLocation: props.initialCursorLocation.richText as Bookmark,
|
initialCursorLocation: props.initialCursorLocation.richText as Bookmark,
|
||||||
onCursorUpdate: props.onCursorMotion,
|
onCursorUpdate: props.onCursorMotion,
|
||||||
editor,
|
editor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const noteChangeTimeRef = useRef(Date.now());
|
||||||
const lastNoteIdRef = useRef(props.noteId);
|
const lastNoteIdRef = useRef(props.noteId);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return () => {};
|
if (!editor) return () => {};
|
||||||
@@ -1070,6 +1069,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
|||||||
// Use nextOnChangeEventInfo's noteId -- lastOnChangeEventInfo can be slightly out-of-date.
|
// Use nextOnChangeEventInfo's noteId -- lastOnChangeEventInfo can be slightly out-of-date.
|
||||||
const differentNoteId = lastNoteIdRef.current !== props.noteId;
|
const differentNoteId = lastNoteIdRef.current !== props.noteId;
|
||||||
const differentContent = lastOnChangeEventInfo.current.content !== props.content;
|
const differentContent = lastOnChangeEventInfo.current.content !== props.content;
|
||||||
|
|
||||||
|
if (differentNoteId) noteChangeTimeRef.current = Date.now();
|
||||||
|
|
||||||
if (differentNoteId || differentContent || !resourcesEqual) {
|
if (differentNoteId || differentContent || !resourcesEqual) {
|
||||||
const result = await props.markupToHtml(
|
const result = await props.markupToHtml(
|
||||||
props.contentMarkupLanguage,
|
props.contentMarkupLanguage,
|
||||||
@@ -1120,8 +1122,12 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
|||||||
// times would result in an empty note.
|
// times would result in an empty note.
|
||||||
// https://github.com/laurent22/joplin/issues/3534
|
// https://github.com/laurent22/joplin/issues/3534
|
||||||
editor.undoManager.reset();
|
editor.undoManager.reset();
|
||||||
|
|
||||||
|
// Only restore the cursor position from the global state when switching notes.
|
||||||
|
// See https://github.com/laurent22/joplin/issues/13579
|
||||||
|
onRestoreCursorPosition();
|
||||||
} else {
|
} else {
|
||||||
// Restore the cursor location
|
// Restore the cursor location from the current note
|
||||||
editor.selection.bookmarkManager.moveToBookmark(bookmark);
|
editor.selection.bookmarkManager.moveToBookmark(bookmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,6 +1136,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
|||||||
resourceInfos: props.resourceInfos,
|
resourceInfos: props.resourceInfos,
|
||||||
contentKey: props.contentKey,
|
contentKey: props.contentKey,
|
||||||
};
|
};
|
||||||
|
props_onMessage.current({ channel: 'noteRenderComplete' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const allAssetsOptions: NoteStyleOptions = {
|
const allAssetsOptions: NoteStyleOptions = {
|
||||||
@@ -1143,7 +1150,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
|||||||
await loadDocumentAssets(props.themeId, editor, allAssets);
|
await loadDocumentAssets(props.themeId, editor, allAssets);
|
||||||
|
|
||||||
dispatchDidUpdate(editor);
|
dispatchDidUpdate(editor);
|
||||||
onInitialContentSet();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void loadContent();
|
void loadContent();
|
||||||
@@ -1338,7 +1344,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
|||||||
// keep it this way for now.
|
// keep it this way for now.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
function onKeyUp(event: any) {
|
function onKeyUp(event: any) {
|
||||||
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key)) {
|
const timeSinceNoteChange = Date.now() - noteChangeTimeRef.current;
|
||||||
|
|
||||||
|
// A key that is pressed before the editor is opened, and that is released after it is
|
||||||
|
// opened is going to be processed here. For example if the user presses Enter in
|
||||||
|
// GotoAnything to arrive here. But in that case, we don't want the change handler to be
|
||||||
|
// activated, because that would change the note timestamp. So we take into account how
|
||||||
|
// long the note has been loaded before we process the key. Fixes
|
||||||
|
// https://github.com/laurent22/joplin/issues/12367
|
||||||
|
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key) && timeSinceNoteChange > 200) {
|
||||||
onChangeHandler();
|
onChangeHandler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const useCursorPositioning = ({ initialCursorLocation, editor, onCursorUpdate }:
|
|||||||
initialCursorLocationRef.current = initialCursorLocation;
|
initialCursorLocationRef.current = initialCursorLocation;
|
||||||
|
|
||||||
const appliedInitialCursorLocationRef = useRef(false);
|
const appliedInitialCursorLocationRef = useRef(false);
|
||||||
const onInitialContentSet = useCallback(() => {
|
const onRestoreCursorPosition = useCallback(() => {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
if (initialCursorLocationRef.current) {
|
if (initialCursorLocationRef.current) {
|
||||||
editor.selection.moveToBookmark(initialCursorLocationRef.current);
|
editor.selection.moveToBookmark(initialCursorLocationRef.current);
|
||||||
@@ -26,8 +26,6 @@ const useCursorPositioning = ({ initialCursorLocation, editor, onCursorUpdate }:
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return () => {};
|
if (!editor) return () => {};
|
||||||
|
|
||||||
editor.on('ContentSet', onInitialContentSet);
|
|
||||||
|
|
||||||
const onSelectionChange = () => {
|
const onSelectionChange = () => {
|
||||||
// Wait until the initial cursor position has been set. This avoids resetting
|
// Wait until the initial cursor position has been set. This avoids resetting
|
||||||
// the initial cursor position to zero when the editor first loads.
|
// the initial cursor position to zero when the editor first loads.
|
||||||
@@ -44,12 +42,11 @@ const useCursorPositioning = ({ initialCursorLocation, editor, onCursorUpdate }:
|
|||||||
editor.on('SelectionChange', onSelectionChange);
|
editor.on('SelectionChange', onSelectionChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
editor.off('ContentSet', onInitialContentSet);
|
|
||||||
editor.off('SelectionChange', onSelectionChange);
|
editor.off('SelectionChange', onSelectionChange);
|
||||||
};
|
};
|
||||||
}, [editor, onCursorUpdate, onInitialContentSet]);
|
}, [editor, onCursorUpdate, onRestoreCursorPosition]);
|
||||||
|
|
||||||
return { onInitialContentSet };
|
return { onRestoreCursorPosition };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useCursorPositioning;
|
export default useCursorPositioning;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { MarkupToHtmlHandler } from '../../../utils/types';
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import enableTextAreaTab, { TextAreaTabHandler } from './enableTextAreaTab';
|
import enableTextAreaTab, { TextAreaTabHandler } from './enableTextAreaTab';
|
||||||
import { MarkupToHtml } from '@joplin/renderer';
|
import { MarkupToHtml } from '@joplin/renderer';
|
||||||
|
import { getGlobalSettings } from '@joplin/renderer/types';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
@@ -90,7 +92,7 @@ function openEditDialog(
|
|||||||
onSubmit: async (dialogApi: any) => {
|
onSubmit: async (dialogApi: any) => {
|
||||||
const newSource = newBlockSource(dialogApi.getData().languageInput, dialogApi.getData().codeTextArea, source);
|
const newSource = newBlockSource(dialogApi.getData().languageInput, dialogApi.getData().codeTextArea, source);
|
||||||
const md = `${newSource.openCharacters}${newSource.content}${newSource.closeCharacters}`;
|
const md = `${newSource.openCharacters}${newSource.content}${newSource.closeCharacters}`;
|
||||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, md, { bodyOnly: true });
|
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, md, { bodyOnly: true, globalSettings: getGlobalSettings(Setting) });
|
||||||
|
|
||||||
// markupToHtml will return the complete editable HTML, but we only
|
// markupToHtml will return the complete editable HTML, but we only
|
||||||
// want to update the inner HTML, so as not to break additional props that
|
// want to update the inner HTML, so as not to break additional props that
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { NoteEditorProps, FormNote, OnChangeEvent, AllAssetsOptions, NoteBodyEdi
|
|||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import Button, { ButtonLevel } from '../Button/Button';
|
import Button, { ButtonLevel } from '../Button/Button';
|
||||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||||
import { AppState, EditorCursorLocations } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
import { _, _n } from '@joplin/lib/locale';
|
import { _, _n } from '@joplin/lib/locale';
|
||||||
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
||||||
@@ -58,6 +58,7 @@ import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisibleP
|
|||||||
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
||||||
import getResourceBaseUrl from './utils/getResourceBaseUrl';
|
import getResourceBaseUrl from './utils/getResourceBaseUrl';
|
||||||
import useInitialCursorLocation from './utils/useInitialCursorLocation';
|
import useInitialCursorLocation from './utils/useInitialCursorLocation';
|
||||||
|
import NotePositionService, { EditorCursorLocations } from '@joplin/lib/services/NotePositionService';
|
||||||
|
|
||||||
const debounce = require('debounce');
|
const debounce = require('debounce');
|
||||||
|
|
||||||
@@ -333,8 +334,8 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
const { scrollWhenReadyRef, clearScrollWhenReady } = useScrollWhenReadyOptions({
|
const { scrollWhenReadyRef, clearScrollWhenReady } = useScrollWhenReadyOptions({
|
||||||
noteId: formNote.id,
|
noteId: formNote.id,
|
||||||
selectedNoteHash: props.selectedNoteHash,
|
selectedNoteHash: props.selectedNoteHash,
|
||||||
lastEditorScrollPercents: props.lastEditorScrollPercents,
|
|
||||||
editorRef,
|
editorRef,
|
||||||
|
editorName: props.bodyEditor,
|
||||||
});
|
});
|
||||||
const onMessage = useMessageHandler(scrollWhenReadyRef, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
const onMessage = useMessageHandler(scrollWhenReadyRef, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||||
|
|
||||||
@@ -400,23 +401,14 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
}, [setShowRevisions]);
|
}, [setShowRevisions]);
|
||||||
|
|
||||||
const onScroll = useCallback((event: { percent: number }) => {
|
const onScroll = useCallback((event: { percent: number }) => {
|
||||||
props.dispatch({
|
const noteId = formNoteRef.current.id;
|
||||||
type: 'EDITOR_SCROLL_PERCENT_SET',
|
NotePositionService.instance().updateScrollPosition(noteId, windowId, event.percent);
|
||||||
// In callbacks of setTimeout()/setInterval(), props/state cannot be used
|
}, [windowId]);
|
||||||
// to refer the current value, since they would be one or more generations old.
|
|
||||||
// For the purpose, useRef value should be used.
|
|
||||||
noteId: formNoteRef.current.id,
|
|
||||||
percent: event.percent,
|
|
||||||
});
|
|
||||||
}, [props.dispatch]);
|
|
||||||
|
|
||||||
const onCursorMotion = useCallback((location: EditorCursorLocations) => {
|
const onCursorMotion = useCallback((location: EditorCursorLocations) => {
|
||||||
props.dispatch({
|
const noteId = formNoteRef.current.id;
|
||||||
type: 'EDITOR_CURSOR_POSITION_SET',
|
NotePositionService.instance().updateCursorPosition(noteId, windowId, location);
|
||||||
noteId: formNoteRef.current.id,
|
}, [windowId]);
|
||||||
location,
|
|
||||||
});
|
|
||||||
}, [props.dispatch]);
|
|
||||||
|
|
||||||
function renderNoNotes(rootStyle: React.CSSProperties) {
|
function renderNoNotes(rootStyle: React.CSSProperties) {
|
||||||
const emptyDivStyle = {
|
const emptyDivStyle = {
|
||||||
@@ -429,7 +421,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
|||||||
|
|
||||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||||
const initialCursorLocation = useInitialCursorLocation({
|
const initialCursorLocation = useInitialCursorLocation({
|
||||||
lastEditorCursorLocations: props.lastEditorCursorLocations, noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const markupLanguage = formNote.markup_language;
|
const markupLanguage = formNote.markup_language;
|
||||||
@@ -742,8 +734,6 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
|||||||
watchedNoteFiles: state.watchedNoteFiles,
|
watchedNoteFiles: state.watchedNoteFiles,
|
||||||
notesParentType: windowState.notesParentType,
|
notesParentType: windowState.notesParentType,
|
||||||
selectedNoteTags: windowState.selectedNoteTags,
|
selectedNoteTags: windowState.selectedNoteTags,
|
||||||
lastEditorScrollPercents: state.lastEditorScrollPercents,
|
|
||||||
lastEditorCursorLocations: state.lastEditorCursorLocations,
|
|
||||||
selectedNoteHash: windowState.selectedNoteHash,
|
selectedNoteHash: windowState.selectedNoteHash,
|
||||||
searches: state.searches,
|
searches: state.searches,
|
||||||
selectedSearchId: windowState.selectedSearchId,
|
selectedSearchId: windowState.selectedSearchId,
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { LinkRenderingType } from '@joplin/renderer/MdToHtml';
|
import { LinkRenderingType } from '@joplin/renderer/MdToHtml';
|
||||||
import { MarkupToHtmlOptions } from './types';
|
import { MarkupToHtmlOptions } from './types';
|
||||||
|
import { getGlobalSettings, ResourceInfos } from '@joplin/renderer/types';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
|
interface OptionOverride {
|
||||||
|
bodyOnly: boolean;
|
||||||
|
resourceInfos?: ResourceInfos;
|
||||||
|
allowedFilePrefixes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (override: OptionOverride = null): MarkupToHtmlOptions => {
|
||||||
return {
|
return {
|
||||||
plugins: {
|
plugins: {
|
||||||
checkbox: {
|
checkbox: {
|
||||||
@@ -12,6 +20,7 @@ export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
replaceResourceInternalToExternalLinks: true,
|
replaceResourceInternalToExternalLinks: true,
|
||||||
|
globalSettings: getGlobalSettings(Setting),
|
||||||
...override,
|
...override,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ export async function getResourcesFromPasteEvent(event: any) {
|
|||||||
const formatType = format.split('/')[0];
|
const formatType = format.split('/')[0];
|
||||||
|
|
||||||
if (formatType === 'image') {
|
if (formatType === 'image') {
|
||||||
|
// writeImageToFile can process only image/jpeg, image/jpg or image/png mime types
|
||||||
|
if (['image/png', 'image/jpg', 'image/jpeg'].indexOf(format) < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (event) event.preventDefault();
|
if (event) event.preventDefault();
|
||||||
|
|
||||||
const image = clipboard.readImage();
|
const image = clipboard.readImage();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
|||||||
import { RefObject, SetStateAction } from 'react';
|
import { RefObject, SetStateAction } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
||||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations, NoteIdToScrollPercent } from '../../../app.reducer';
|
import { EditorCursorLocations } from '@joplin/lib/services/NotePositionService';
|
||||||
|
|
||||||
export interface AllAssetsOptions {
|
export interface AllAssetsOptions {
|
||||||
contentMaxWidthTarget?: string;
|
contentMaxWidthTarget?: string;
|
||||||
@@ -41,8 +41,6 @@ export interface NoteEditorProps {
|
|||||||
notesParentType: string;
|
notesParentType: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
selectedNoteTags: any[];
|
selectedNoteTags: any[];
|
||||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
|
||||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
|
||||||
selectedNoteHash: string;
|
selectedNoteHash: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
searches: any[];
|
searches: any[];
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations } from '../../../app.reducer';
|
import { WindowIdContext } from '../../NewWindowOrIFrame';
|
||||||
|
import NotePositionService from '@joplin/lib/services/NotePositionService';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
|
||||||
noteId: string;
|
noteId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useInitialCursorLocation = ({ noteId, lastEditorCursorLocations }: Props) => {
|
const useInitialCursorLocation = ({ noteId }: Props) => {
|
||||||
const lastCursorLocation = lastEditorCursorLocations[noteId];
|
const windowId = useContext(WindowIdContext);
|
||||||
|
|
||||||
return useMemo((): EditorCursorLocations => {
|
return useMemo(() => {
|
||||||
return lastCursorLocation ?? { };
|
return NotePositionService.instance().getCursorPosition(noteId, windowId);
|
||||||
}, [lastCursorLocation]);
|
}, [noteId, windowId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useInitialCursorLocation;
|
export default useInitialCursorLocation;
|
||||||
|
|||||||
@@ -1,38 +1,43 @@
|
|||||||
import { RefObject, useCallback, useRef } from 'react';
|
import { RefObject, useCallback, useContext, useRef } from 'react';
|
||||||
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
||||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||||
import type { NoteIdToScrollPercent } from '../../../app.reducer';
|
import NotePositionService from '@joplin/lib/services/NotePositionService';
|
||||||
import useNowEffect from '@joplin/lib/hooks/useNowEffect';
|
import useNowEffect from '@joplin/lib/hooks/useNowEffect';
|
||||||
|
import { WindowIdContext } from '../../NewWindowOrIFrame';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
|
editorName: string;
|
||||||
selectedNoteHash: string;
|
selectedNoteHash: string;
|
||||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
|
||||||
editorRef: RefObject<NoteBodyEditorRef>;
|
editorRef: RefObject<NoteBodyEditorRef>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollPercents, editorRef }: Props) => {
|
const useScrollWhenReadyOptions = ({ noteId, editorName, selectedNoteHash, editorRef }: Props) => {
|
||||||
const scrollWhenReadyRef = useRef<ScrollOptions|null>(null);
|
const scrollWhenReadyRef = useRef<ScrollOptions|null>(null);
|
||||||
|
|
||||||
const previousNoteId = usePrevious(noteId);
|
const previousNoteId = usePrevious(noteId);
|
||||||
const lastScrollPercentsRef = useRef<NoteIdToScrollPercent>(null);
|
const previousEditor = usePrevious(editorName);
|
||||||
lastScrollPercentsRef.current = lastEditorScrollPercents;
|
const windowId = useContext(WindowIdContext);
|
||||||
|
|
||||||
|
|
||||||
// This needs to be a nowEffect to prevent race conditions
|
// This needs to be a nowEffect to prevent race conditions
|
||||||
useNowEffect(() => {
|
useNowEffect(() => {
|
||||||
if (noteId === previousNoteId) return () => {};
|
const editorChanged = editorName !== previousEditor;
|
||||||
|
const noteIdChanged = noteId !== previousNoteId;
|
||||||
|
if (!editorChanged && !noteIdChanged) return () => {};
|
||||||
|
|
||||||
|
const lastScrollPercent = NotePositionService.instance().getScrollPercent(noteId, windowId) || 0;
|
||||||
|
scrollWhenReadyRef.current = {
|
||||||
|
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
||||||
|
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
|
||||||
|
};
|
||||||
|
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
editorRef.current.resetScroll();
|
editorRef.current.resetScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastScrollPercent = lastScrollPercentsRef.current[noteId] || 0;
|
|
||||||
scrollWhenReadyRef.current = {
|
|
||||||
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
|
||||||
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
|
|
||||||
};
|
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
|
}, [editorName, previousEditor, noteId, previousNoteId, selectedNoteHash, editorRef, windowId]);
|
||||||
|
|
||||||
const clearScrollWhenReady = useCallback(() => {
|
const clearScrollWhenReady = useCallback(() => {
|
||||||
scrollWhenReadyRef.current = null;
|
scrollWhenReadyRef.current = null;
|
||||||
|
|||||||
@@ -501,7 +501,12 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
|||||||
<div role='table' aria-labelledby='note-properties-dialog-title'>
|
<div role='table' aria-labelledby='note-properties-dialog-title'>
|
||||||
{noteComps}
|
{noteComps}
|
||||||
</div>
|
</div>
|
||||||
<DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
|
<DialogButtonRow
|
||||||
|
themeId={this.props.themeId}
|
||||||
|
okButtonShow={!this.isReadOnly()}
|
||||||
|
okButtonRef={this.okButton}
|
||||||
|
onClick={this.buttonRow_click}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
|||||||
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
import useDeleteHistoryClick from '@joplin/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick';
|
import useDeleteHistoryClick from '@joplin/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick';
|
||||||
|
import { getGlobalSettings } from '@joplin/renderer/types';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
@@ -72,6 +74,7 @@ const useNoteContent = (
|
|||||||
const result = await markupToHtml(markupLanguage, noteBody, {
|
const result = await markupToHtml(markupLanguage, noteBody, {
|
||||||
resources: await shared.attachedResources(noteBody),
|
resources: await shared.attachedResources(noteBody),
|
||||||
whiteBackgroundNoteRendering: markupLanguage === MarkupLanguage.Html,
|
whiteBackgroundNoteRendering: markupLanguage === MarkupLanguage.Html,
|
||||||
|
globalSettings: getGlobalSettings(Setting),
|
||||||
});
|
});
|
||||||
|
|
||||||
viewerRef.current.setHtml(result.html, {
|
viewerRef.current.setHtml(result.html, {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { createContext, useMemo, useRef, useState } from 'react';
|
import { createContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { NotificationType, PopupHandle, PopupControl as PopupManager } from './types';
|
import { NotificationType, PopupHandle, PopupControl as PopupManager } from './types';
|
||||||
import { Hour, msleep } from '@joplin/utils/time';
|
import { Hour, msleep } from '@joplin/utils/time';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
|
||||||
export const PopupNotificationContext = createContext<PopupManager|null>(null);
|
export const PopupNotificationContext = createContext<PopupManager|null>(null);
|
||||||
export const VisibleNotificationsContext = createContext<PopupSpec[]>([]);
|
export const VisibleNotificationsContext = createContext<PopupSpec[]>([]);
|
||||||
@@ -112,6 +113,18 @@ const PopupNotificationProvider: React.FC<Props> = props => {
|
|||||||
return manager;
|
return manager;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const defaultShowToast = shim.showToast;
|
||||||
|
shim.showToast = async (message: string, options) => {
|
||||||
|
const popup = popupManager.createPopup(() => message, { type: options?.type ?? NotificationType.Info });
|
||||||
|
popup.scheduleDismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
shim.showToast = defaultShowToast;
|
||||||
|
};
|
||||||
|
}, [popupManager]);
|
||||||
|
|
||||||
return <PopupNotificationContext.Provider value={popupManager}>
|
return <PopupNotificationContext.Provider value={popupManager}>
|
||||||
<VisibleNotificationsContext.Provider value={popupSpecs}>
|
<VisibleNotificationsContext.Provider value={popupSpecs}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ToastType } from '@joplin/lib/shim';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
export type PopupHandle = {
|
export type PopupHandle = {
|
||||||
@@ -5,14 +6,13 @@ export type PopupHandle = {
|
|||||||
scheduleDismiss(delay?: number): void;
|
scheduleDismiss(delay?: number): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum NotificationType {
|
|
||||||
Info = 'info',
|
|
||||||
Success = 'success',
|
|
||||||
Error = 'error',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NotificationContentCallback = ()=> React.ReactNode;
|
export type NotificationContentCallback = ()=> React.ReactNode;
|
||||||
|
|
||||||
|
// NotificationType is an alias for ToastType
|
||||||
|
export type NotificationType = ToastType;
|
||||||
|
// eslint-disable-next-line no-redeclare -- export const is necessary for creating an alias, this is not a redeclaration.
|
||||||
|
export const NotificationType = ToastType;
|
||||||
|
|
||||||
export interface PopupOptions {
|
export interface PopupOptions {
|
||||||
type?: NotificationType;
|
type?: NotificationType;
|
||||||
}
|
}
|
||||||
|
|||||||
47
packages/app-desktop/gui/ProfileEditor.scss
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.profile-management {
|
||||||
|
font-family: var(--joplin-font-family);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .tableContainer {
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
color: var(--joplin-color);
|
||||||
|
|
||||||
|
> .notification {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-table {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> thead > tr > .headerCell {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> tbody > tr {
|
||||||
|
> .nameCell {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 1px;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .dataCell {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
color: var(--joplin-color-faded);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .profileActions > button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
193
packages/app-desktop/gui/ProfileEditor.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useState, useEffect, CSSProperties } from 'react';
|
||||||
|
import ButtonBar from './ConfigScreen/ButtonBar';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import bridge from '../services/bridge';
|
||||||
|
import dialogs from './dialogs';
|
||||||
|
import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types';
|
||||||
|
import { deleteProfileById, saveProfileConfig } from '@joplin/lib/services/profileConfig';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import { AppState } from '../app.reducer';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
|
||||||
|
const logger = Logger.create('ProfileEditor');
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
dispatch: Dispatch;
|
||||||
|
style: CSSProperties;
|
||||||
|
profileConfig: ProfileConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileTableProps {
|
||||||
|
profiles: Profile[];
|
||||||
|
currentProfileId: string;
|
||||||
|
onProfileRename: (profile: Profile)=> void;
|
||||||
|
onProfileDelete: (profile: Profile)=> void;
|
||||||
|
themeId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileTableComp: React.FC<ProfileTableProps> = props => {
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="profile-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="headerCell">{_('Profile name')}</th>
|
||||||
|
<th className="headerCell">{_('ID')}</th>
|
||||||
|
<th className="headerCell">{_('Status')}</th>
|
||||||
|
<th className="headerCell">{_('Actions')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{props.profiles.map((profile: Profile, index: number) => {
|
||||||
|
const isCurrentProfile = profile.id === props.currentProfileId;
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td id={`name-${profile.id}`} className="nameCell">
|
||||||
|
<span style={{ fontWeight: isCurrentProfile ? 'bold' : 'normal' }}>
|
||||||
|
{profile.name || `(${_('Untitled')})`}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="dataCell">{profile.id}</td>
|
||||||
|
<td className="dataCell">
|
||||||
|
{isCurrentProfile ? _('Active') : ''}
|
||||||
|
</td>
|
||||||
|
<td className="dataCell profileActions">
|
||||||
|
<button
|
||||||
|
id={`rename-${profile.id}`}
|
||||||
|
aria-labelledby={`rename-${profile.id} name-${profile.id}`}
|
||||||
|
style={theme.buttonStyle}
|
||||||
|
onClick={() => props.onProfileRename(profile)}
|
||||||
|
>
|
||||||
|
{_('Rename')}
|
||||||
|
</button>
|
||||||
|
{!isCurrentProfile && (
|
||||||
|
<button
|
||||||
|
id={`delete-${profile.id}`}
|
||||||
|
aria-labelledby={`delete-${profile.id} name-${profile.id}`}
|
||||||
|
style={theme.buttonStyle}
|
||||||
|
onClick={() => props.onProfileDelete(profile)}
|
||||||
|
>
|
||||||
|
{_('Delete')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfileEditorComponent: React.FC<Props> = props => {
|
||||||
|
const { profileConfig, themeId, dispatch } = props;
|
||||||
|
const theme = themeStyle(themeId);
|
||||||
|
const style = props.style;
|
||||||
|
const containerHeight = style.height;
|
||||||
|
|
||||||
|
const [profiles, setProfiles] = useState<Profile[]>(profileConfig.profiles);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProfiles(profileConfig.profiles);
|
||||||
|
}, [profileConfig]);
|
||||||
|
|
||||||
|
const saveNewProfileConfig = async (makeNewProfileConfig: ()=> ProfileConfig) => {
|
||||||
|
try {
|
||||||
|
const newProfileConfig = makeNewProfileConfig();
|
||||||
|
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newProfileConfig);
|
||||||
|
dispatch({
|
||||||
|
type: 'PROFILE_CONFIG_SET',
|
||||||
|
value: newProfileConfig,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
bridge().showErrorMessageBox(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onProfileRename = async (profile: Profile) => {
|
||||||
|
const newName = await dialogs.prompt(_('Profile name:'), '', profile.name);
|
||||||
|
if (newName === null || newName === undefined || newName === profile.name) return;
|
||||||
|
|
||||||
|
if (!newName.trim()) {
|
||||||
|
bridge().showErrorMessageBox(_('Profile name cannot be empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeNewProfileConfig = () => {
|
||||||
|
const newProfiles = profileConfig.profiles.map(p =>
|
||||||
|
p.id === profile.id ? { ...p, name: newName.trim() } : p,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newProfileConfig = {
|
||||||
|
...profileConfig,
|
||||||
|
profiles: newProfiles,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newProfileConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveNewProfileConfig(makeNewProfileConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onProfileDelete = async (profile: Profile) => {
|
||||||
|
const isCurrentProfile = profile.id === profileConfig.currentProfileId;
|
||||||
|
if (isCurrentProfile) {
|
||||||
|
bridge().showErrorMessageBox(_('The active profile cannot be deleted. Switch to a different profile and try again.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = bridge().showConfirmMessageBox(_('Delete profile "%s"?\n\nAll data, including notes, notebooks and tags will be permanently deleted.', profile.name), {
|
||||||
|
buttons: [_('Delete'), _('Cancel')],
|
||||||
|
defaultId: 1,
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
const rootDir = Setting.value('rootProfileDir');
|
||||||
|
const profileDir = `${rootDir}/profile-${profile.id}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await shim.fsDriver().remove(profileDir);
|
||||||
|
logger.info('Deleted profile directory: ', profileDir);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting profile directory: ', error);
|
||||||
|
bridge().showErrorMessageBox(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="profile-management" style={{ ...theme.containerStyle, height: containerHeight }}>
|
||||||
|
<div className="tableContainer">
|
||||||
|
<div className="notification" style={theme.notificationBox}>
|
||||||
|
{_('Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.')}
|
||||||
|
</div>
|
||||||
|
<ProfileTableComp
|
||||||
|
themeId={themeId}
|
||||||
|
profiles={profiles}
|
||||||
|
currentProfileId={profileConfig.currentProfileId}
|
||||||
|
onProfileRename={onProfileRename}
|
||||||
|
onProfileDelete={onProfileDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ButtonBar
|
||||||
|
onCancelClick={() => dispatch({ type: 'NAV_BACK' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => ({
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
profileConfig: state.profileConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ProfileEditorComponent);
|
||||||
@@ -84,7 +84,13 @@ const ResourceTableComp = (props: ResourceTable) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredResources = props.resources.filter(
|
const filteredResources = props.resources.filter(
|
||||||
(resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter),
|
(resource: InnerResource) => {
|
||||||
|
if (props.filter) {
|
||||||
|
const filterLowerCase = props.filter.toLowerCase();
|
||||||
|
return resource.title?.toLowerCase().includes(filterLowerCase) || resource.id.toLowerCase().includes(filterLowerCase);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSortableHeader = (title: string, order: SortingOrder) => {
|
const renderSortableHeader = (title: string, order: SortingOrder) => {
|
||||||
@@ -297,7 +303,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
|||||||
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
|
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
|
||||||
_('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.')
|
_('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.')
|
||||||
}</div>
|
}</div>
|
||||||
<div style={{ float: 'right' }}>
|
<p style={{ float: 'left', paddingRight: 10 }}>
|
||||||
<input
|
<input
|
||||||
style={theme.inputStyle}
|
style={theme.inputStyle}
|
||||||
type="search"
|
type="search"
|
||||||
@@ -305,7 +311,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
|||||||
onChange={this.onFilterUpdate}
|
onChange={this.onFilterUpdate}
|
||||||
placeholder={_('Search...')}
|
placeholder={_('Search...')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</p>
|
||||||
{this.state.isLoading && <div>{_('Please wait...')}</div>}
|
{this.state.isLoading && <div>{_('Please wait...')}</div>}
|
||||||
{!this.state.isLoading && <div>
|
{!this.state.isLoading && <div>
|
||||||
{!this.state.resources && <div>
|
{!this.state.resources && <div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import Dialog from './Dialog';
|
|||||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||||
import ImportScreen from './ImportScreen';
|
import ImportScreen from './ImportScreen';
|
||||||
import ResourceScreen from './ResourceScreen';
|
import ResourceScreen from './ResourceScreen';
|
||||||
|
import ProfileEditor from './ProfileEditor';
|
||||||
import Navigator from './Navigator';
|
import Navigator from './Navigator';
|
||||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||||
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
||||||
@@ -71,7 +72,7 @@ async function initialize() {
|
|||||||
panes: Setting.value('noteVisiblePanes'),
|
panes: Setting.value('noteVisiblePanes'),
|
||||||
});
|
});
|
||||||
|
|
||||||
InteropService.instance().document = document;
|
InteropService.instance().domParser = new DOMParser();
|
||||||
InteropService.instance().xmlSerializer = new XMLSerializer();
|
InteropService.instance().xmlSerializer = new XMLSerializer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +166,7 @@ class RootComponent extends React.Component<Props, any> {
|
|||||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||||
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
|
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
|
||||||
|
ProfileEditor: { screen: ProfileEditor, title: () => _('Manage profiles') },
|
||||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Dialog from '../Dialog';
|
import Dialog from '../Dialog';
|
||||||
import DialogButtonRow, { ClickEvent, ButtonSpec } from '../DialogButtonRow';
|
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||||
import DialogTitle from '../DialogTitle';
|
import DialogTitle from '../DialogTitle';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import ShareService, { ApiShare } from '@joplin/lib/services/share/ShareService';
|
import ShareService, { ApiShare } from '@joplin/lib/services/share/ShareService';
|
||||||
@@ -20,6 +20,7 @@ import { reg } from '@joplin/lib/registry';
|
|||||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||||
import { ChangeEvent, Dropdown, DropdownOptions, DropdownVariant } from '../Dropdown/Dropdown';
|
import { ChangeEvent, Dropdown, DropdownOptions, DropdownVariant } from '../Dropdown/Dropdown';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
|
import { SettingsRecord } from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
const logger = Logger.create('ShareFolderDialog');
|
const logger = Logger.create('ShareFolderDialog');
|
||||||
|
|
||||||
@@ -129,7 +130,6 @@ function ShareFolderDialog(props: Props) {
|
|||||||
const [share, setShare] = useState<StateShare>(null);
|
const [share, setShare] = useState<StateShare>(null);
|
||||||
const [shareUsers, setShareUsers] = useState<StateShareUser[]>([]);
|
const [shareUsers, setShareUsers] = useState<StateShareUser[]>([]);
|
||||||
const [shareState, setShareState] = useState<ShareState>(ShareState.Idle);
|
const [shareState, setShareState] = useState<ShareState>(ShareState.Idle);
|
||||||
const [customButtons, setCustomButtons] = useState<ButtonSpec[]>([]);
|
|
||||||
const [recipientsBeingUpdated, setRecipientsBeingUpdated] = useState<Record<string, boolean>>({});
|
const [recipientsBeingUpdated, setRecipientsBeingUpdated] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
async function synchronize(event: AsyncEffectEvent = null) {
|
async function synchronize(event: AsyncEffectEvent = null) {
|
||||||
@@ -163,13 +163,6 @@ function ShareFolderDialog(props: Props) {
|
|||||||
void ShareService.instance().refreshShareUsers(share.id);
|
void ShareService.instance().refreshShareUsers(share.id);
|
||||||
}, [share]);
|
}, [share]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCustomButtons(share ? [{
|
|
||||||
name: 'unshare',
|
|
||||||
label: _('Unshare'),
|
|
||||||
}] : []);
|
|
||||||
}, [share]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!share) return;
|
if (!share) return;
|
||||||
const sus = props.shareUsers[share.id];
|
const sus = props.shareUsers[share.id];
|
||||||
@@ -177,10 +170,6 @@ function ShareFolderDialog(props: Props) {
|
|||||||
setShareUsers(sus);
|
setShareUsers(sus);
|
||||||
}, [share, props.shareUsers]);
|
}, [share, props.shareUsers]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void ShareService.instance().refreshShares();
|
|
||||||
}, [props.folderId]);
|
|
||||||
|
|
||||||
const permissionsFromString = (p: string): SharePermissions => {
|
const permissionsFromString = (p: string): SharePermissions => {
|
||||||
return {
|
return {
|
||||||
can_read: 1,
|
can_read: 1,
|
||||||
@@ -269,7 +258,7 @@ function ShareFolderDialog(props: Props) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function renderAddRecipient() {
|
function renderAddRecipient() {
|
||||||
const disabled = shareState !== ShareState.Idle;
|
const disabled = shareState !== ShareState.Idle && shareState !== ShareState.Synchronizing;
|
||||||
|
|
||||||
const dropdown = !props.canUseSharePermissions ? null : <Dropdown className="permission-dropdown" options={permissionOptions} value={recipientPermissions} onChange={recipientPermissions_change}/>;
|
const dropdown = !props.canUseSharePermissions ? null : <Dropdown className="permission-dropdown" options={permissionOptions} value={recipientPermissions} onChange={recipientPermissions_change}/>;
|
||||||
|
|
||||||
@@ -395,6 +384,17 @@ function ShareFolderDialog(props: Props) {
|
|||||||
props.onClose();
|
props.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customButtons = useMemo(() => {
|
||||||
|
return share ? [{
|
||||||
|
name: 'unshare',
|
||||||
|
label: _('Unshare'),
|
||||||
|
// Don't allow unsharing the folder during the "create" action. Doing so might
|
||||||
|
// be able to cause issues similar to #13518 (e.g. if the "unshare" action completes while
|
||||||
|
// the "share" action is still in progress).
|
||||||
|
disabled: shareState === ShareState.Creating || shareState === ShareState.Synchronizing,
|
||||||
|
}] : [];
|
||||||
|
}, [share, shareState]);
|
||||||
|
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
return (
|
return (
|
||||||
<StyledRoot className="share-folder-dialog">
|
<StyledRoot className="share-folder-dialog">
|
||||||
@@ -422,10 +422,14 @@ function ShareFolderDialog(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => {
|
const mapStateToProps = (state: State) => {
|
||||||
|
const getCanUseSharePermissions = (settings: Partial<SettingsRecord>) => {
|
||||||
|
return [9, 10, 11].includes(settings['sync.target']) && !!settings['sync.10.canUseSharePermissions'];
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shares: state.shareService.shares,
|
shares: state.shareService.shares,
|
||||||
shareUsers: state.shareService.shareUsers,
|
shareUsers: state.shareService.shareUsers,
|
||||||
canUseSharePermissions: state.settings['sync.target'] === 10 && state.settings['sync.10.canUseSharePermissions'],
|
canUseSharePermissions: getCanUseSharePermissions(state.settings),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
|||||||
import { _, _n } from '@joplin/lib/locale';
|
import { _, _n } from '@joplin/lib/locale';
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import DialogButtonRow from './DialogButtonRow';
|
import DialogButtonRow from './DialogButtonRow';
|
||||||
import { themeStyle, buildStyle } from '@joplin/lib/theme';
|
|
||||||
import Dialog from './Dialog';
|
import Dialog from './Dialog';
|
||||||
import DialogTitle from './DialogTitle';
|
import DialogTitle from './DialogTitle';
|
||||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||||
@@ -29,47 +28,6 @@ interface Props {
|
|||||||
syncTargetId: number;
|
syncTargetId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function styles_(props: Props) {
|
|
||||||
return buildStyle('ShareNoteDialog', props.themeId, theme => {
|
|
||||||
return {
|
|
||||||
root: {
|
|
||||||
minWidth: 500,
|
|
||||||
},
|
|
||||||
noteList: {
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
note: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme.dividerColor,
|
|
||||||
padding: '0.5em',
|
|
||||||
marginBottom: 5,
|
|
||||||
},
|
|
||||||
noteTitle: {
|
|
||||||
...theme.textStyle,
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
color: theme.color,
|
|
||||||
},
|
|
||||||
noteRemoveButton: {
|
|
||||||
background: 'none',
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
noteRemoveButtonIcon: {
|
|
||||||
color: theme.color,
|
|
||||||
fontSize: '1.4em',
|
|
||||||
},
|
|
||||||
copyShareLinkButton: {
|
|
||||||
...theme.buttonStyle,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShareNoteDialog(props: Props) {
|
export function ShareNoteDialog(props: Props) {
|
||||||
const [notes, setNotes] = useState<NoteEntity[]>([]);
|
const [notes, setNotes] = useState<NoteEntity[]>([]);
|
||||||
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
|
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
|
||||||
@@ -77,8 +35,6 @@ export function ShareNoteDialog(props: Props) {
|
|||||||
|
|
||||||
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
|
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
|
||||||
const noteCount = notes.length;
|
const noteCount = notes.length;
|
||||||
const theme = themeStyle(props.themeId);
|
|
||||||
const styles = styles_(props);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void ShareService.instance().refreshShares();
|
void ShareService.instance().refreshShares();
|
||||||
@@ -117,8 +73,8 @@ export function ShareNoteDialog(props: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={note.id} style={styles.note}>
|
<div key={note.id} className='shared-note-list-item'>
|
||||||
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
|
<span className='title'>{note.title}</span>{unshareButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -128,7 +84,7 @@ export function ShareNoteDialog(props: Props) {
|
|||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
noteComps.push(renderNote(note));
|
noteComps.push(renderNote(note));
|
||||||
}
|
}
|
||||||
return <div style={styles.noteList}>{noteComps}</div>;
|
return <div className="notes">{noteComps}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusMessage = useShareStatusMessage({ sharesState, noteCount });
|
const statusMessage = useShareStatusMessage({ sharesState, noteCount });
|
||||||
@@ -136,7 +92,7 @@ export function ShareNoteDialog(props: Props) {
|
|||||||
|
|
||||||
function renderEncryptionWarningMessage() {
|
function renderEncryptionWarningMessage() {
|
||||||
if (!encryptionWarning) return null;
|
if (!encryptionWarning) return null;
|
||||||
return <div style={theme.textStyle}>{encryptionWarning}<hr/></div>;
|
return <div className="message">{encryptionWarning}<hr/></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRecursiveShareChange = useCallback(() => {
|
const onRecursiveShareChange = useCallback(() => {
|
||||||
@@ -155,12 +111,16 @@ export function ShareNoteDialog(props: Props) {
|
|||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
return (
|
return (
|
||||||
<div style={styles.root} className="form">
|
<div className="form share-note-dialog">
|
||||||
<DialogTitle title={_('Publish Notes')}/>
|
<DialogTitle title={_('Publish Notes')}/>
|
||||||
{renderNoteList(notes)}
|
{renderNoteList(notes)}
|
||||||
{renderRecursiveShareCheckbox()}
|
{renderRecursiveShareCheckbox()}
|
||||||
<button disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
|
<button
|
||||||
<div style={theme.textStyle}>{statusMessage}</div>
|
disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0}
|
||||||
|
className="share"
|
||||||
|
onClick={shareLinkButton_click}
|
||||||
|
>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
|
||||||
|
<div className="message">{statusMessage}</div>
|
||||||
{renderEncryptionWarningMessage()}
|
{renderEncryptionWarningMessage()}
|
||||||
<DialogButtonRow
|
<DialogButtonRow
|
||||||
themeId={props.themeId}
|
themeId={props.themeId}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useMemo, useRef, useState } from 'react';
|
|||||||
import ItemList from '../ItemList';
|
import ItemList from '../ItemList';
|
||||||
import useElementHeight from '../hooks/useElementHeight';
|
import useElementHeight from '../hooks/useElementHeight';
|
||||||
import useSidebarListData from './hooks/useSidebarListData';
|
import useSidebarListData from './hooks/useSidebarListData';
|
||||||
import useSelectedSidebarIndex from './hooks/useSelectedSidebarIndex';
|
import useSelectedSidebarIndexes from './hooks/useSelectedSidebarIndexes';
|
||||||
import useOnSidebarKeyDownHandler from './hooks/useOnSidebarKeyDownHandler';
|
import useOnSidebarKeyDownHandler from './hooks/useOnSidebarKeyDownHandler';
|
||||||
import useFocusHandler from './hooks/useFocusHandler';
|
import useFocusHandler from './hooks/useFocusHandler';
|
||||||
import useOnRenderItem from './hooks/useOnRenderItem';
|
import useOnRenderItem from './hooks/useOnRenderItem';
|
||||||
@@ -26,7 +26,9 @@ interface Props {
|
|||||||
tags: TagsWithNoteCountEntity[];
|
tags: TagsWithNoteCountEntity[];
|
||||||
folders: FolderEntity[];
|
folders: FolderEntity[];
|
||||||
notesParentType: string;
|
notesParentType: string;
|
||||||
|
selectedTagIds: string[];
|
||||||
selectedTagId: string;
|
selectedTagId: string;
|
||||||
|
selectedFolderIds: string[];
|
||||||
selectedFolderId: string;
|
selectedFolderId: string;
|
||||||
selectedSmartFilterId: string;
|
selectedSmartFilterId: string;
|
||||||
collapsedFolderIds: string[];
|
collapsedFolderIds: string[];
|
||||||
@@ -37,7 +39,7 @@ interface Props {
|
|||||||
|
|
||||||
const FolderAndTagList: React.FC<Props> = props => {
|
const FolderAndTagList: React.FC<Props> = props => {
|
||||||
const listItems = useSidebarListData(props);
|
const listItems = useSidebarListData(props);
|
||||||
const { selectedIndex, updateSelectedIndex } = useSelectedSidebarIndex({
|
const { selectedIndex, selectedIndexes, updateSelectedIndex } = useSelectedSidebarIndexes({
|
||||||
...props,
|
...props,
|
||||||
listItems: listItems,
|
listItems: listItems,
|
||||||
});
|
});
|
||||||
@@ -50,6 +52,7 @@ const FolderAndTagList: React.FC<Props> = props => {
|
|||||||
const onRenderItem = useOnRenderItem({
|
const onRenderItem = useOnRenderItem({
|
||||||
...props,
|
...props,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
|
selectedIndexes,
|
||||||
listItems,
|
listItems,
|
||||||
containerRef: listContainerRef,
|
containerRef: listContainerRef,
|
||||||
});
|
});
|
||||||
@@ -58,6 +61,7 @@ const FolderAndTagList: React.FC<Props> = props => {
|
|||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
listItems: listItems,
|
listItems: listItems,
|
||||||
selectedIndex,
|
selectedIndex,
|
||||||
|
selectedIndexes,
|
||||||
updateSelectedIndex,
|
updateSelectedIndex,
|
||||||
collapsedFolderIds: props.collapsedFolderIds,
|
collapsedFolderIds: props.collapsedFolderIds,
|
||||||
});
|
});
|
||||||
@@ -107,6 +111,8 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
tags: state.tags,
|
tags: state.tags,
|
||||||
folders: state.folders,
|
folders: state.folders,
|
||||||
notesParentType: mainWindowState.notesParentType,
|
notesParentType: mainWindowState.notesParentType,
|
||||||
|
selectedFolderIds: mainWindowState.selectedFolderIds,
|
||||||
|
selectedTagIds: mainWindowState.selectedTagIds,
|
||||||
selectedFolderId: mainWindowState.selectedFolderId,
|
selectedFolderId: mainWindowState.selectedFolderId,
|
||||||
selectedTagId: mainWindowState.selectedTagId,
|
selectedTagId: mainWindowState.selectedTagId,
|
||||||
collapsedFolderIds: state.collapsedFolderIds,
|
collapsedFolderIds: state.collapsedFolderIds,
|
||||||
|
|||||||
65
packages/app-desktop/gui/Sidebar/hooks/useOnItemClick.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { MouseEvent } from 'react';
|
||||||
|
import { ModelType } from '@joplin/lib/BaseModel';
|
||||||
|
import { RefObject, useCallback } from 'react';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { ListItem, ListItemType } from '../types';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
|
||||||
|
export interface ItemClickEvent {
|
||||||
|
id: string;
|
||||||
|
type: ModelType;
|
||||||
|
event: MouseEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
itemsRef: RefObject<ListItem[]>;
|
||||||
|
selectedIndexesRef: RefObject<number[]>;
|
||||||
|
dispatch: Dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItemToId = (item: ListItem) => {
|
||||||
|
if (item.kind === ListItemType.Tag) return item.tag.id;
|
||||||
|
if (item.kind === ListItemType.Folder) return item.folder.id;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useOnItemClick = ({ dispatch, selectedIndexesRef, itemsRef }: Props) => {
|
||||||
|
return useCallback(({ id, type, event }: ItemClickEvent) => {
|
||||||
|
const action = type === ModelType.Folder ? 'FOLDER_SELECT' : 'TAG_SELECT';
|
||||||
|
const selectedIndexes = selectedIndexesRef.current;
|
||||||
|
const findItemIndex = () => itemsRef.current.findIndex(item => listItemToId(item) === id);
|
||||||
|
|
||||||
|
if (event.shiftKey && selectedIndexes.length > 0) {
|
||||||
|
const index = findItemIndex();
|
||||||
|
if (index === -1) throw new Error(`No item found with ID: ${id}`);
|
||||||
|
|
||||||
|
const lastAddedIndex = selectedIndexes[selectedIndexes.length - 1];
|
||||||
|
const indexStart = Math.min(index, lastAddedIndex);
|
||||||
|
const indexStop = Math.max(index, lastAddedIndex);
|
||||||
|
const itemIds = itemsRef.current.slice(indexStart, indexStop + 1)
|
||||||
|
.map(listItemToId)
|
||||||
|
.filter(id => !!id);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: `${action}_ADD`,
|
||||||
|
ids: itemIds,
|
||||||
|
});
|
||||||
|
} else if (shim.isMac() ? event.metaKey : event.ctrlKey) {
|
||||||
|
const index = findItemIndex();
|
||||||
|
// Don't allow unselecting all items: Keep at least one item selected
|
||||||
|
const canDeselect = selectedIndexes.length > 1;
|
||||||
|
const actionType = canDeselect && selectedIndexes.includes(index) ? 'REMOVE' : 'ADD';
|
||||||
|
dispatch({
|
||||||
|
type: `${action}_${actionType}`,
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: action,
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [dispatch, selectedIndexesRef, itemsRef]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOnItemClick;
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { DragEventHandler, MouseEventHandler, useCallback, useMemo, useRef } from 'react';
|
import { DragEventHandler, MouseEventHandler, useCallback, useMemo, useRef } from 'react';
|
||||||
import { ItemClickListener, ItemDragListener, ListItem, ListItemType } from '../types';
|
import { ItemClickListener, ItemDragListener, ListItem, ListItemType } from '../types';
|
||||||
import TagItem, { TagLinkClickEvent } from '../listItemComponents/TagItem';
|
import TagItem from '../listItemComponents/TagItem';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { clipboard } from 'electron';
|
import { clipboard } from 'electron';
|
||||||
|
import type { MenuItem as MenuItemType } from 'electron';
|
||||||
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
||||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||||
import Tag from '@joplin/lib/models/Tag';
|
import Tag from '@joplin/lib/models/Tag';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||||
import { AppState } from '../../../app.reducer';
|
|
||||||
import { store } from '@joplin/lib/reducer';
|
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import bridge from '../../../services/bridge';
|
import bridge from '../../../services/bridge';
|
||||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||||
@@ -18,7 +17,6 @@ import CommandService from '@joplin/lib/services/CommandService';
|
|||||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||||
import InteropServiceHelper from '../../../InteropServiceHelper';
|
import InteropServiceHelper from '../../../InteropServiceHelper';
|
||||||
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
|
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSortOrderService';
|
import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSortOrderService';
|
||||||
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||||
@@ -29,12 +27,13 @@ import Logger from '@joplin/utils/Logger';
|
|||||||
import onFolderDrop from '@joplin/lib/models/utils/onFolderDrop';
|
import onFolderDrop from '@joplin/lib/models/utils/onFolderDrop';
|
||||||
import HeaderItem from '../listItemComponents/HeaderItem';
|
import HeaderItem from '../listItemComponents/HeaderItem';
|
||||||
import AllNotesItem from '../listItemComponents/AllNotesItem';
|
import AllNotesItem from '../listItemComponents/AllNotesItem';
|
||||||
import ListItemWrapper from '../listItemComponents/ListItemWrapper';
|
import ListItemWrapper, { ItemSelectionState } from '../listItemComponents/ListItemWrapper';
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
|
import useOnItemClick from './useOnItemClick';
|
||||||
|
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
const MenuItem = bridge().MenuItem;
|
const MenuItem: typeof MenuItemType = bridge().MenuItem;
|
||||||
|
|
||||||
const logger = Logger.create('useOnRenderItem');
|
const logger = Logger.create('useOnRenderItem');
|
||||||
|
|
||||||
@@ -47,6 +46,7 @@ interface Props {
|
|||||||
containerRef: React.RefObject<HTMLDivElement>;
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
selectedIndex: number;
|
selectedIndex: number;
|
||||||
|
selectedIndexes: number[];
|
||||||
listItems: ListItem[];
|
listItems: ListItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +65,11 @@ const focusListItem = (item: HTMLElement|null) => {
|
|||||||
|
|
||||||
const noFocusListItem = () => {};
|
const noFocusListItem = () => {};
|
||||||
|
|
||||||
|
const folderCommandToMenuItem = (commandId: string, folderIds: string|string[]) => {
|
||||||
|
const options = Array.isArray(folderIds) ? { commandFolderIds: folderIds } : { commandFolderId: folderIds };
|
||||||
|
return new MenuItem(menuUtils.commandToStatefulMenuItem(commandId, folderIds, options));
|
||||||
|
};
|
||||||
|
|
||||||
const useOnRenderItem = (props: Props) => {
|
const useOnRenderItem = (props: Props) => {
|
||||||
|
|
||||||
const pluginsRef = useRef<PluginStates>(null);
|
const pluginsRef = useRef<PluginStates>(null);
|
||||||
@@ -72,13 +77,6 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
const foldersRef = useRef<FolderEntity[]>(null);
|
const foldersRef = useRef<FolderEntity[]>(null);
|
||||||
foldersRef.current = props.folders;
|
foldersRef.current = props.folders;
|
||||||
|
|
||||||
const tagItem_click = useCallback(({ tag }: TagLinkClickEvent) => {
|
|
||||||
props.dispatch({
|
|
||||||
type: 'TAG_SELECT',
|
|
||||||
id: tag ? tag.id : null,
|
|
||||||
});
|
|
||||||
}, [props.dispatch]);
|
|
||||||
|
|
||||||
const onTagDrop_: DragEventHandler<HTMLElement> = useCallback(async event => {
|
const onTagDrop_: DragEventHandler<HTMLElement> = useCallback(async event => {
|
||||||
const tagId = event.currentTarget.getAttribute('data-tag-id');
|
const tagId = event.currentTarget.getAttribute('data-tag-id');
|
||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
@@ -94,6 +92,24 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const selectedIndexesRef = useRef(props.selectedIndexes);
|
||||||
|
selectedIndexesRef.current = props.selectedIndexes;
|
||||||
|
const itemsRef = useRef(props.listItems);
|
||||||
|
itemsRef.current = props.listItems;
|
||||||
|
const getSelectedIds = useCallback(() => {
|
||||||
|
return selectedIndexesRef.current.map(index => {
|
||||||
|
const item = itemsRef.current[index];
|
||||||
|
if (item.kind === ListItemType.Folder) {
|
||||||
|
return item.folder.id;
|
||||||
|
} else if (item.kind === ListItemType.Tag) {
|
||||||
|
return item.tag.id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(id => !!id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onItemClick = useOnItemClick({ dispatch: props.dispatch, selectedIndexesRef, itemsRef });
|
||||||
|
|
||||||
const onItemContextMenu: ItemContextMenuListener = useCallback(async event => {
|
const onItemContextMenu: ItemContextMenuListener = useCallback(async event => {
|
||||||
const itemId = event.currentTarget.getAttribute('data-id');
|
const itemId = event.currentTarget.getAttribute('data-id');
|
||||||
if (itemId === Folder.conflictFolderId()) return;
|
if (itemId === Folder.conflictFolderId()) return;
|
||||||
@@ -101,14 +117,22 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
const itemType = Number(event.currentTarget.getAttribute('data-type'));
|
const itemType = Number(event.currentTarget.getAttribute('data-type'));
|
||||||
if (!itemId || !itemType) throw new Error('No data on element');
|
if (!itemId || !itemType) throw new Error('No data on element');
|
||||||
|
|
||||||
const state: AppState = store().getState();
|
let itemIds = [itemId];
|
||||||
|
const itemIndex = Number(event.currentTarget.getAttribute('data-index'));
|
||||||
|
if (selectedIndexesRef.current.includes(itemIndex)) {
|
||||||
|
itemIds = getSelectedIds();
|
||||||
|
}
|
||||||
|
|
||||||
let deleteMessage = '';
|
let deleteMessage = '';
|
||||||
const deleteButtonLabel = _('Remove');
|
const deleteButtonLabel = _('Remove');
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_TAG) {
|
if (itemType === BaseModel.TYPE_TAG) {
|
||||||
const tag = await Tag.load(itemId);
|
if (itemIds.length === 1) {
|
||||||
deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32));
|
const tag = await Tag.load(itemId);
|
||||||
|
deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32));
|
||||||
|
} else {
|
||||||
|
deleteMessage = _('Remove %d tags from all notes? This cannot be undone.', itemIds.length);
|
||||||
|
}
|
||||||
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
||||||
deleteMessage = _('Remove this search from the sidebar?');
|
deleteMessage = _('Remove this search from the sidebar?');
|
||||||
}
|
}
|
||||||
@@ -131,16 +155,13 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
const isDeleted = item ? !!item.deleted_time : false;
|
const isDeleted = item ? !!item.deleted_time : false;
|
||||||
|
|
||||||
if (!isDeleted) {
|
if (!isDeleted) {
|
||||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
const isDecryptedFolder = itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied;
|
||||||
menu.append(
|
if (isDecryptedFolder && itemIds.length === 1) {
|
||||||
new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder', itemId)),
|
menu.append(folderCommandToMenuItem('newFolder', itemId));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||||
menu.append(
|
menu.append(folderCommandToMenuItem('deleteFolder', itemIds));
|
||||||
new MenuItem(menuUtils.commandToStatefulMenuItem('deleteFolder', itemId)),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
@@ -153,7 +174,9 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_TAG) {
|
if (itemType === BaseModel.TYPE_TAG) {
|
||||||
await Tag.untagAll(itemId);
|
for (const itemId of itemIds) {
|
||||||
|
await Tag.untagAll(itemId);
|
||||||
|
}
|
||||||
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
||||||
props.dispatch({
|
props.dispatch({
|
||||||
type: 'SEARCH_DELETE',
|
type: 'SEARCH_DELETE',
|
||||||
@@ -165,15 +188,18 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
if (isDecryptedFolder) {
|
||||||
|
const whenClause = CommandService.instance().currentWhenClauseContext({ commandFolderIds: itemIds });
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
...menuUtils.commandToStatefulMenuItem('moveToFolder', [itemId]),
|
...menuUtils.commandToStatefulMenuItem('moveToFolder', itemIds),
|
||||||
// By default, enabled is based on the selected folder. However, the right-click
|
// By default, moveToFolder's enabled condition is based on the selected notes. However, the right-click
|
||||||
// menu can be shown for unselected folders.
|
// menu item applies to folders. For now, use a custom condition:
|
||||||
enabled: true,
|
enabled: !whenClause.foldersIncludeReadOnly,
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', { folderId: itemId })));
|
if (isDecryptedFolder && itemIds.length === 1) {
|
||||||
|
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', { folderId: itemId }, { commandFolderId: itemId })));
|
||||||
|
|
||||||
menu.append(new MenuItem({ type: 'separator' }));
|
menu.append(new MenuItem({ type: 'separator' }));
|
||||||
|
|
||||||
@@ -188,25 +214,17 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: module.fullLabel(),
|
label: module.fullLabel(),
|
||||||
click: async () => {
|
click: async () => {
|
||||||
await InteropServiceHelper.export(props.dispatch, module, { sourceFolderIds: [itemId], plugins: pluginsRef.current });
|
await InteropServiceHelper.export(props.dispatch, module, { sourceFolderIds: itemIds, plugins: pluginsRef.current });
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't display the "Share notebook" menu item for sub-notebooks
|
// Only show the share/leave share actions for top-level folders
|
||||||
// that are within a shared notebook. If user wants to do this,
|
const shareFolderItem = folderCommandToMenuItem('showShareFolderDialog', itemId);
|
||||||
// they'd have to move the notebook out of the shared notebook
|
if (shareFolderItem.enabled) menu.append(shareFolderItem);
|
||||||
// first.
|
const leaveSharedFolderItem = folderCommandToMenuItem('leaveSharedFolder', itemId);
|
||||||
const whenClause = stateToWhenClauseContext(state, { commandFolderId: itemId });
|
if (leaveSharedFolderItem.enabled) menu.append(leaveSharedFolderItem);
|
||||||
|
|
||||||
if (CommandService.instance().isEnabled('showShareFolderDialog', whenClause)) {
|
|
||||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('showShareFolderDialog', itemId)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CommandService.instance().isEnabled('leaveSharedFolder', whenClause)) {
|
|
||||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('leaveSharedFolder', itemId)));
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
@@ -216,14 +234,14 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
);
|
);
|
||||||
if (Setting.value('notes.perFolderSortOrderEnabled')) {
|
if (Setting.value('notes.perFolderSortOrderEnabled')) {
|
||||||
menu.append(new MenuItem({
|
menu.append(new MenuItem({
|
||||||
...menuUtils.commandToStatefulMenuItem('togglePerFolderSortOrder', itemId),
|
...menuUtils.commandToStatefulMenuItem('togglePerFolderSortOrder', itemId, { commandFolderId: itemId }),
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
checked: PerFolderSortOrderService.isSet(itemId),
|
checked: PerFolderSortOrderService.isSet(itemId),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
if (itemType === BaseModel.TYPE_FOLDER && itemIds.length === 1) {
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: _('Copy external link'),
|
label: _('Copy external link'),
|
||||||
@@ -234,7 +252,7 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_TAG) {
|
if (itemType === BaseModel.TYPE_TAG && itemIds.length === 1) {
|
||||||
menu.append(new MenuItem(
|
menu.append(new MenuItem(
|
||||||
menuUtils.commandToStatefulMenuItem('renameTag', itemId),
|
menuUtils.commandToStatefulMenuItem('renameTag', itemId),
|
||||||
));
|
));
|
||||||
@@ -253,24 +271,22 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
for (const view of pluginViews) {
|
for (const view of pluginViews) {
|
||||||
const location = view.location;
|
const location = view.location;
|
||||||
|
|
||||||
if (itemType === ModelType.Tag && location === MenuItemLocation.TagContextMenu ||
|
if (itemType === ModelType.Tag && location === MenuItemLocation.TagContextMenu) {
|
||||||
itemType === ModelType.Folder && location === MenuItemLocation.FolderContextMenu
|
|
||||||
) {
|
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem(menuUtils.commandToStatefulMenuItem(view.commandName, itemId)),
|
new MenuItem(menuUtils.commandToStatefulMenuItem(view.commandName, itemId)),
|
||||||
);
|
);
|
||||||
|
} else if (itemType === ModelType.Folder && location === MenuItemLocation.FolderContextMenu) {
|
||||||
|
menu.append(folderCommandToMenuItem(view.commandName, itemId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||||
menu.append(
|
menu.append(folderCommandToMenuItem('restoreFolder', itemIds));
|
||||||
new MenuItem(menuUtils.commandToStatefulMenuItem('restoreFolder', itemId)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.popup({ window: bridge().activeWindow() });
|
menu.popup({ window: bridge().activeWindow() });
|
||||||
}, [props.dispatch, pluginsRef]);
|
}, [props.dispatch, pluginsRef, getSelectedIds]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -278,10 +294,16 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
||||||
if (!folderId) return;
|
if (!folderId) return;
|
||||||
|
|
||||||
|
let itemIds = [folderId];
|
||||||
|
const itemIndex = Number(event.currentTarget.getAttribute('data-index'));
|
||||||
|
if (selectedIndexesRef.current.includes(itemIndex)) {
|
||||||
|
itemIds = getSelectedIds();
|
||||||
|
}
|
||||||
|
|
||||||
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
||||||
event.dataTransfer.clearData();
|
event.dataTransfer.clearData();
|
||||||
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
|
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify(itemIds));
|
||||||
}, []);
|
}, [getSelectedIds]);
|
||||||
|
|
||||||
const onFolderDragOver_: ItemDragListener = useCallback(event => {
|
const onFolderDragOver_: ItemDragListener = useCallback(event => {
|
||||||
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
|
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
|
||||||
@@ -323,13 +345,6 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
});
|
});
|
||||||
}, [props.dispatch]);
|
}, [props.dispatch]);
|
||||||
|
|
||||||
const folderItem_click = useCallback((folderId: string) => {
|
|
||||||
props.dispatch({
|
|
||||||
type: 'FOLDER_SELECT',
|
|
||||||
id: folderId ? folderId : null,
|
|
||||||
});
|
|
||||||
}, [props.dispatch]);
|
|
||||||
|
|
||||||
// If at least one of the folder has an icon, then we display icons for all
|
// If at least one of the folder has an icon, then we display icons for all
|
||||||
// folders (those without one will get the default icon). This is so that
|
// folders (those without one will get the default icon). This is so that
|
||||||
// visual alignment is correct for all folders, otherwise the folder tree
|
// visual alignment is correct for all folders, otherwise the folder tree
|
||||||
@@ -338,22 +353,26 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
return Folder.shouldShowFolderIcons(props.folders);
|
return Folder.shouldShowFolderIcons(props.folders);
|
||||||
}, [props.folders]);
|
}, [props.folders]);
|
||||||
|
|
||||||
const selectedIndexRef = useRef(props.selectedIndex);
|
|
||||||
selectedIndexRef.current = props.selectedIndex;
|
|
||||||
|
|
||||||
const itemCount = props.listItems.length;
|
const itemCount = props.listItems.length;
|
||||||
return useCallback((item: ListItem, index: number) => {
|
return useCallback((item: ListItem, index: number) => {
|
||||||
const selected = props.selectedIndex === index;
|
const primarySelected = props.selectedIndex === index;
|
||||||
|
const selected = primarySelected || props.selectedIndexes.includes(index);
|
||||||
|
const selectionState: ItemSelectionState = {
|
||||||
|
primarySelected,
|
||||||
|
selected,
|
||||||
|
multipleItemsSelected: props.selectedIndexes.length > 1,
|
||||||
|
};
|
||||||
|
|
||||||
const focusInList = document.hasFocus() && props.containerRef.current?.contains(document.activeElement);
|
const focusInList = document.hasFocus() && props.containerRef.current?.contains(document.activeElement);
|
||||||
const anchorRef = (focusInList && selected) ? focusListItem : noFocusListItem;
|
const anchorRef = (focusInList && primarySelected) ? focusListItem : noFocusListItem;
|
||||||
|
|
||||||
if (item.kind === ListItemType.Tag) {
|
if (item.kind === ListItemType.Tag) {
|
||||||
const tag = item.tag;
|
const tag = item.tag;
|
||||||
return <TagItem
|
return <TagItem
|
||||||
key={item.key}
|
key={item.key}
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
selected={selected}
|
selectionState={selectionState}
|
||||||
onClick={tagItem_click}
|
onClick={onItemClick}
|
||||||
onTagDrop={onTagDrop_}
|
onTagDrop={onTagDrop_}
|
||||||
onContextMenu={onItemContextMenu}
|
onContextMenu={onItemContextMenu}
|
||||||
label={item.label}
|
label={item.label}
|
||||||
@@ -383,7 +402,7 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
return <FolderItem
|
return <FolderItem
|
||||||
key={item.key}
|
key={item.key}
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
selected={selected}
|
selectionState={selectionState}
|
||||||
folderId={folder.id}
|
folderId={folder.id}
|
||||||
folderTitle={item.label}
|
folderTitle={item.label}
|
||||||
folderIcon={Folder.unserializeIcon(folder.icon)}
|
folderIcon={Folder.unserializeIcon(folder.icon)}
|
||||||
@@ -395,7 +414,7 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
onFolderDragOver_={onFolderDragOver_}
|
onFolderDragOver_={onFolderDragOver_}
|
||||||
onFolderDrop_={onFolderDrop_}
|
onFolderDrop_={onFolderDrop_}
|
||||||
itemContextMenu={onItemContextMenu}
|
itemContextMenu={onItemContextMenu}
|
||||||
folderItem_click={folderItem_click}
|
folderItem_click={onItemClick}
|
||||||
onFolderToggleClick_={onFolderToggleClick_}
|
onFolderToggleClick_={onFolderToggleClick_}
|
||||||
shareId={folder.share_id}
|
shareId={folder.share_id}
|
||||||
parentId={folder.parent_id}
|
parentId={folder.parent_id}
|
||||||
@@ -408,7 +427,7 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
item={item}
|
item={item}
|
||||||
isSelected={selected}
|
selectionState={selectionState}
|
||||||
onDrop={item.supportsFolderDrop ? onFolderDrop_ : null}
|
onDrop={item.supportsFolderDrop ? onFolderDrop_ : null}
|
||||||
index={index}
|
index={index}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@@ -417,7 +436,7 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
return <AllNotesItem
|
return <AllNotesItem
|
||||||
key={item.key}
|
key={item.key}
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
selected={selected}
|
selectionState={selectionState}
|
||||||
item={item}
|
item={item}
|
||||||
index={index}
|
index={index}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
@@ -428,7 +447,7 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
containerRef={anchorRef}
|
containerRef={anchorRef}
|
||||||
depth={1}
|
depth={1}
|
||||||
selected={selected}
|
selectionState={selectionState}
|
||||||
itemIndex={index}
|
itemIndex={index}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
highlightOnHover={false}
|
highlightOnHover={false}
|
||||||
@@ -442,7 +461,7 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
return exhaustivenessCheck;
|
return exhaustivenessCheck;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
folderItem_click,
|
onItemClick,
|
||||||
onFolderDragOver_,
|
onFolderDragOver_,
|
||||||
onFolderDragStart_,
|
onFolderDragStart_,
|
||||||
onFolderDrop_,
|
onFolderDrop_,
|
||||||
@@ -452,8 +471,8 @@ const useOnRenderItem = (props: Props) => {
|
|||||||
props.collapsedFolderIds,
|
props.collapsedFolderIds,
|
||||||
props.folders,
|
props.folders,
|
||||||
showFolderIcons,
|
showFolderIcons,
|
||||||
tagItem_click,
|
|
||||||
props.selectedIndex,
|
props.selectedIndex,
|
||||||
|
props.selectedIndexes,
|
||||||
props.containerRef,
|
props.containerRef,
|
||||||
itemCount,
|
itemCount,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface Props {
|
|||||||
listItems: ListItem[];
|
listItems: ListItem[];
|
||||||
collapsedFolderIds: string[];
|
collapsedFolderIds: string[];
|
||||||
selectedIndex: number;
|
selectedIndex: number;
|
||||||
|
selectedIndexes: number[];
|
||||||
updateSelectedIndex: SetSelectedIndexCallback;
|
updateSelectedIndex: SetSelectedIndexCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +69,14 @@ const findNextTypeAheadMatch = (selectedIndex: number, query: string, listItems:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useOnSidebarKeyDownHandler = (props: Props) => {
|
const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||||
const { updateSelectedIndex, listItems, selectedIndex, collapsedFolderIds, dispatch } = props;
|
const { updateSelectedIndex, listItems, selectedIndex, selectedIndexes, collapsedFolderIds, dispatch } = props;
|
||||||
|
|
||||||
return useCallback<KeyboardEventHandler<HTMLElement>>((event) => {
|
return useCallback<KeyboardEventHandler<HTMLElement>>((event) => {
|
||||||
const selectedItem = listItems[selectedIndex];
|
const selectedItem = listItems[selectedIndex];
|
||||||
let indexChange = 0;
|
let indexChange = 0;
|
||||||
|
|
||||||
|
const ctrlAltOrMeta = event.ctrlKey || event.altKey || event.metaKey;
|
||||||
|
|
||||||
if (selectedItem && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) {
|
if (selectedItem && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -102,16 +105,19 @@ const useOnSidebarKeyDownHandler = (props: Props) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.code === 'Home') {
|
} else if (event.code === 'Home') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateSelectedIndex(0);
|
updateSelectedIndex(0, { extend: false });
|
||||||
indexChange = 0;
|
indexChange = 0;
|
||||||
} else if (event.code === 'End') {
|
} else if (event.code === 'End') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateSelectedIndex(listItems.length - 1);
|
updateSelectedIndex(listItems.length - 1, { extend: false });
|
||||||
indexChange = 0;
|
indexChange = 0;
|
||||||
|
} else if (event.code === 'Escape' && selectedIndexes.length > 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
updateSelectedIndex(selectedIndex, { extend: false });
|
||||||
} else if (event.code === 'Enter' && !event.shiftKey) {
|
} else if (event.code === 'Enter' && !event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void CommandService.instance().execute('focusElement', 'noteList');
|
void CommandService.instance().execute('focusElement', 'noteList');
|
||||||
} else if (selectedIndex && selectedIndex >= 0 && event.key.length === 1) {
|
} else if (selectedIndex && selectedIndex >= 0 && event.key.length === 1 && !ctrlAltOrMeta) {
|
||||||
const nextMatch = findNextTypeAheadMatch(selectedIndex, event.key, listItems);
|
const nextMatch = findNextTypeAheadMatch(selectedIndex, event.key, listItems);
|
||||||
if (nextMatch !== -1) {
|
if (nextMatch !== -1) {
|
||||||
indexChange = nextMatch - selectedIndex;
|
indexChange = nextMatch - selectedIndex;
|
||||||
@@ -120,9 +126,9 @@ const useOnSidebarKeyDownHandler = (props: Props) => {
|
|||||||
|
|
||||||
if (indexChange !== 0) {
|
if (indexChange !== 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateSelectedIndex(selectedIndex + indexChange);
|
updateSelectedIndex(selectedIndex + indexChange, { extend: event.shiftKey });
|
||||||
}
|
}
|
||||||
}, [selectedIndex, collapsedFolderIds, listItems, updateSelectedIndex, dispatch]);
|
}, [selectedIndex, selectedIndexes, collapsedFolderIds, listItems, updateSelectedIndex, dispatch]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useOnSidebarKeyDownHandler;
|
export default useOnSidebarKeyDownHandler;
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { ListItem, ListItemType } from '../types';
|
|
||||||
import { isFolderSelected, isTagSelected } from '@joplin/lib/components/shared/side-menu-shared';
|
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
|
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
dispatch: Dispatch;
|
|
||||||
listItems: ListItem[];
|
|
||||||
|
|
||||||
notesParentType: string;
|
|
||||||
selectedTagId: string;
|
|
||||||
selectedFolderId: string;
|
|
||||||
selectedSmartFilterId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useSelectedSidebarIndex = (props: Props) => {
|
|
||||||
const appStateSelectedIndex = useMemo(() => {
|
|
||||||
for (let i = 0; i < props.listItems.length; i++) {
|
|
||||||
const listItem = props.listItems[i];
|
|
||||||
|
|
||||||
let selected = false;
|
|
||||||
if (listItem.kind === ListItemType.AllNotes) {
|
|
||||||
selected = props.selectedSmartFilterId === ALL_NOTES_FILTER_ID && props.notesParentType === 'SmartFilter';
|
|
||||||
} else if (listItem.kind === ListItemType.Header || listItem.kind === ListItemType.Spacer) {
|
|
||||||
selected = false;
|
|
||||||
} else if (listItem.kind === ListItemType.Folder) {
|
|
||||||
selected = isFolderSelected(listItem.folder, { selectedFolderId: props.selectedFolderId, notesParentType: props.notesParentType });
|
|
||||||
} else if (listItem.kind === ListItemType.Tag) {
|
|
||||||
selected = isTagSelected(listItem.tag, { selectedTagId: props.selectedTagId, notesParentType: props.notesParentType });
|
|
||||||
} else {
|
|
||||||
const exhaustivenessCheck: never = listItem;
|
|
||||||
return exhaustivenessCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}, [props.listItems, props.selectedFolderId, props.selectedTagId, props.selectedSmartFilterId, props.notesParentType]);
|
|
||||||
|
|
||||||
// Not all list items correspond with selectable Joplin folders/tags, but we want to
|
|
||||||
// be able to select them anyway. This is handled with selectedIndexOverride.
|
|
||||||
//
|
|
||||||
// When selectedIndexOverride >= 0, it corresponds to the index of a selected item with no
|
|
||||||
// specific note parent item (e.g. a header).
|
|
||||||
const [selectedIndexOverride, setSelectedIndexOverride] = useState(-1);
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedIndexOverride(-1);
|
|
||||||
}, [appStateSelectedIndex]);
|
|
||||||
|
|
||||||
const updateSelectedIndex = useCallback((newIndex: number) => {
|
|
||||||
if (newIndex < 0) {
|
|
||||||
newIndex = 0;
|
|
||||||
} else if (newIndex >= props.listItems.length) {
|
|
||||||
newIndex = props.listItems.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newItem = props.listItems[newIndex];
|
|
||||||
let newOverrideIndex = -1;
|
|
||||||
if (newItem.kind === ListItemType.AllNotes) {
|
|
||||||
props.dispatch({
|
|
||||||
type: 'SMART_FILTER_SELECT',
|
|
||||||
id: ALL_NOTES_FILTER_ID,
|
|
||||||
});
|
|
||||||
} else if (newItem.kind === ListItemType.Folder) {
|
|
||||||
props.dispatch({
|
|
||||||
type: 'FOLDER_SELECT',
|
|
||||||
id: newItem.folder.id,
|
|
||||||
});
|
|
||||||
} else if (newItem.kind === ListItemType.Tag) {
|
|
||||||
props.dispatch({
|
|
||||||
type: 'TAG_SELECT',
|
|
||||||
id: newItem.tag.id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
newOverrideIndex = newIndex;
|
|
||||||
}
|
|
||||||
setSelectedIndexOverride(newOverrideIndex);
|
|
||||||
}, [props.listItems, props.dispatch]);
|
|
||||||
|
|
||||||
const selectedIndex = selectedIndexOverride === -1 ? appStateSelectedIndex : selectedIndexOverride;
|
|
||||||
return { selectedIndex, updateSelectedIndex };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSelectedSidebarIndex;
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { ListItem, ListItemType } from '../types';
|
||||||
|
import { isFolderSelected, isTagSelected } from '@joplin/lib/components/shared/side-menu-shared';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
|
||||||
|
|
||||||
|
type UpdateSelectedIndexOptions = { extend: boolean };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dispatch: Dispatch;
|
||||||
|
listItems: ListItem[];
|
||||||
|
|
||||||
|
notesParentType: string;
|
||||||
|
selectedTagId: string;
|
||||||
|
selectedTagIds: string[];
|
||||||
|
selectedFolderId: string;
|
||||||
|
selectedFolderIds: string[];
|
||||||
|
selectedSmartFilterId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSelectedSidebarIndexes = (props: Props) => {
|
||||||
|
const isIndexInSelection = useCallback((index: number) => {
|
||||||
|
const listItem = props.listItems[index];
|
||||||
|
|
||||||
|
let selected = false;
|
||||||
|
if (listItem.kind === ListItemType.AllNotes) {
|
||||||
|
selected = props.selectedSmartFilterId === ALL_NOTES_FILTER_ID && props.notesParentType === 'SmartFilter';
|
||||||
|
} else if (listItem.kind === ListItemType.Header || listItem.kind === ListItemType.Spacer) {
|
||||||
|
selected = false;
|
||||||
|
} else if (listItem.kind === ListItemType.Folder) {
|
||||||
|
selected = isFolderSelected(listItem.folder, {
|
||||||
|
selectedFolderIds: props.selectedFolderIds,
|
||||||
|
notesParentType: props.notesParentType,
|
||||||
|
});
|
||||||
|
} else if (listItem.kind === ListItemType.Tag) {
|
||||||
|
selected = isTagSelected(listItem.tag, { selectedTagIds: props.selectedTagIds, notesParentType: props.notesParentType });
|
||||||
|
} else {
|
||||||
|
const exhaustivenessCheck: never = listItem;
|
||||||
|
return exhaustivenessCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
}, [props.listItems, props.selectedFolderIds, props.selectedTagIds, props.selectedSmartFilterId, props.notesParentType]);
|
||||||
|
|
||||||
|
const isIndexPrimarySelected = useCallback((index: number) => {
|
||||||
|
const listItem = props.listItems[index];
|
||||||
|
|
||||||
|
if (listItem.kind === ListItemType.Folder) {
|
||||||
|
return isFolderSelected(listItem.folder, {
|
||||||
|
selectedFolderIds: [props.selectedFolderId],
|
||||||
|
notesParentType: props.notesParentType,
|
||||||
|
});
|
||||||
|
} else if (listItem.kind === ListItemType.Tag) {
|
||||||
|
return isTagSelected(listItem.tag, { selectedTagIds: [props.selectedTagId], notesParentType: props.notesParentType });
|
||||||
|
} else {
|
||||||
|
return isIndexInSelection(index);
|
||||||
|
}
|
||||||
|
}, [props.listItems, isIndexInSelection, props.selectedFolderId, props.selectedTagId, props.notesParentType]);
|
||||||
|
|
||||||
|
const appStateSelectedIndexes = useMemo(() => {
|
||||||
|
const selectedIndexes = [];
|
||||||
|
for (let i = 0; i < props.listItems.length; i++) {
|
||||||
|
if (isIndexInSelection(i)) {
|
||||||
|
selectedIndexes.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedIndexes;
|
||||||
|
}, [props.listItems, isIndexInSelection]);
|
||||||
|
|
||||||
|
const appStateSelectedIndex = useMemo(() => {
|
||||||
|
return props.listItems.findIndex((_item, index) => isIndexPrimarySelected(index));
|
||||||
|
}, [props.listItems, isIndexPrimarySelected]);
|
||||||
|
|
||||||
|
// The main index of all selected indexes. This is where the focus will go.
|
||||||
|
// Ignored if not included in appStateSelectedIndexes.
|
||||||
|
const [primarySelectedIndex, setPrimarySelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
// Not all list items correspond with selectable Joplin folders/tags, but we want to
|
||||||
|
// be able to select them anyway. This is handled with selectedIndexOverride.
|
||||||
|
//
|
||||||
|
// When selectedIndexOverride >= 0, it corresponds to the index of a selected item with no
|
||||||
|
// specific note parent item (e.g. a header).
|
||||||
|
const [selectedIndexOverride, setSelectedIndexOverride] = useState(-1);
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIndexOverride(-1);
|
||||||
|
setPrimarySelectedIndex(appStateSelectedIndex);
|
||||||
|
}, [appStateSelectedIndex]);
|
||||||
|
|
||||||
|
const updateSelectedIndex = useCallback((newIndex: number, options: UpdateSelectedIndexOptions) => {
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex = 0;
|
||||||
|
} else if (newIndex >= props.listItems.length) {
|
||||||
|
newIndex = props.listItems.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItem = props.listItems[newIndex];
|
||||||
|
let newOverrideIndex = -1;
|
||||||
|
if (newItem.kind === ListItemType.AllNotes) {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'SMART_FILTER_SELECT',
|
||||||
|
id: ALL_NOTES_FILTER_ID,
|
||||||
|
});
|
||||||
|
} else if (newItem.kind === ListItemType.Folder) {
|
||||||
|
props.dispatch({
|
||||||
|
type: options.extend ? 'FOLDER_SELECT_ADD' : 'FOLDER_SELECT',
|
||||||
|
id: newItem.folder.id,
|
||||||
|
});
|
||||||
|
} else if (newItem.kind === ListItemType.Tag) {
|
||||||
|
props.dispatch({
|
||||||
|
type: options.extend ? 'TAG_SELECT_ADD' : 'TAG_SELECT',
|
||||||
|
id: newItem.tag.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newOverrideIndex = newIndex;
|
||||||
|
}
|
||||||
|
setSelectedIndexOverride(newOverrideIndex);
|
||||||
|
setPrimarySelectedIndex(newIndex);
|
||||||
|
}, [props.listItems, props.dispatch]);
|
||||||
|
|
||||||
|
const selectedIndexes = useMemo(() => {
|
||||||
|
return selectedIndexOverride === -1 ? appStateSelectedIndexes : [selectedIndexOverride];
|
||||||
|
}, [appStateSelectedIndexes, selectedIndexOverride]);
|
||||||
|
const selectedIndex = selectedIndexes.includes(primarySelectedIndex) ? primarySelectedIndex : (selectedIndexes[0] ?? -1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedIndex,
|
||||||
|
selectedIndexes,
|
||||||
|
updateSelectedIndex,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useSelectedSidebarIndexes;
|
||||||
@@ -9,7 +9,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
|||||||
import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSortOrderService';
|
import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSortOrderService';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import EmptyExpandLink from './EmptyExpandLink';
|
import EmptyExpandLink from './EmptyExpandLink';
|
||||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
import ListItemWrapper, { ItemSelectionState, ListItemRef } from './ListItemWrapper';
|
||||||
import { ListItem } from '../types';
|
import { ListItem } from '../types';
|
||||||
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
|
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids');
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ const MenuItem = bridge().MenuItem;
|
|||||||
interface Props {
|
interface Props {
|
||||||
dispatch: Dispatch;
|
dispatch: Dispatch;
|
||||||
anchorRef: ListItemRef;
|
anchorRef: ListItemRef;
|
||||||
selected: boolean;
|
selectionState: ItemSelectionState;
|
||||||
item: ListItem;
|
item: ListItem;
|
||||||
index: number;
|
index: number;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
@@ -53,7 +53,7 @@ const AllNotesItem: React.FC<Props> = props => {
|
|||||||
<ListItemWrapper
|
<ListItemWrapper
|
||||||
containerRef={props.anchorRef}
|
containerRef={props.anchorRef}
|
||||||
key="allNotesHeader"
|
key="allNotesHeader"
|
||||||
selected={props.selected}
|
selectionState={props.selectionState}
|
||||||
depth={props.item.depth}
|
depth={props.item.depth}
|
||||||
className={'list-item-container list-item-depth-0 all-notes'}
|
className={'list-item-container list-item-depth-0 all-notes'}
|
||||||
highlightOnHover={true}
|
highlightOnHover={true}
|
||||||
@@ -65,7 +65,7 @@ const AllNotesItem: React.FC<Props> = props => {
|
|||||||
<StyledListItemAnchor
|
<StyledListItemAnchor
|
||||||
className="list-item"
|
className="list-item"
|
||||||
isSpecialItem={true}
|
isSpecialItem={true}
|
||||||
selected={props.selected}
|
selected={props.selectionState.selected}
|
||||||
onClick={onAllNotesClick_}
|
onClick={onAllNotesClick_}
|
||||||
onContextMenu={toggleAllNotesContextMenu}
|
onContextMenu={toggleAllNotesContextMenu}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import Folder from '@joplin/lib/models/Folder';
|
|||||||
import { ModelType } from '@joplin/lib/BaseModel';
|
import { ModelType } from '@joplin/lib/BaseModel';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import NoteCount from './NoteCount';
|
import NoteCount from './NoteCount';
|
||||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
import ListItemWrapper, { ItemSelectionState, ListItemRef } from './ListItemWrapper';
|
||||||
import { useId } from 'react';
|
import { useId } from 'react';
|
||||||
|
import { ItemClickEvent } from '../hooks/useOnItemClick';
|
||||||
|
|
||||||
const renderFolderIcon = (folderIcon: FolderIcon) => {
|
const renderFolderIcon = (folderIcon: FolderIcon) => {
|
||||||
if (!folderIcon) {
|
if (!folderIcon) {
|
||||||
@@ -42,17 +43,17 @@ interface FolderItemProps {
|
|||||||
onFolderDragOver_: ItemDragListener;
|
onFolderDragOver_: ItemDragListener;
|
||||||
onFolderDrop_: ItemDragListener;
|
onFolderDrop_: ItemDragListener;
|
||||||
itemContextMenu: ItemContextMenuListener;
|
itemContextMenu: ItemContextMenuListener;
|
||||||
folderItem_click: (folderId: string)=> void;
|
folderItem_click: (event: ItemClickEvent)=> void;
|
||||||
onFolderToggleClick_: ItemClickListener;
|
onFolderToggleClick_: ItemClickListener;
|
||||||
shareId: string;
|
shareId: string;
|
||||||
selected: boolean;
|
selectionState: ItemSelectionState;
|
||||||
|
|
||||||
index: number;
|
index: number;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FolderItem(props: FolderItemProps) {
|
function FolderItem(props: FolderItemProps) {
|
||||||
const { hasChildren, showFolderIcon, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
|
const { hasChildren, showFolderIcon, isExpanded, parentId, depth, selectionState, folderId, folderTitle, folderIcon, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
|
||||||
|
|
||||||
const shareTitle = _('Shared');
|
const shareTitle = _('Shared');
|
||||||
const shareIcon = shareId && !parentId ? <StyledShareIcon aria-label={shareTitle} title={shareTitle} className="fas fa-share-alt"/> : null;
|
const shareIcon = shareId && !parentId ? <StyledShareIcon aria-label={shareTitle} title={shareTitle} className="fas fa-share-alt"/> : null;
|
||||||
@@ -73,11 +74,11 @@ function FolderItem(props: FolderItemProps) {
|
|||||||
containerRef={props.anchorRef}
|
containerRef={props.anchorRef}
|
||||||
// Folders are contained within the "Notebooks" section (which has depth 0):
|
// Folders are contained within the "Notebooks" section (which has depth 0):
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
selected={selected}
|
selectionState={selectionState}
|
||||||
itemIndex={props.index}
|
itemIndex={props.index}
|
||||||
itemCount={props.itemCount}
|
itemCount={props.itemCount}
|
||||||
expanded={hasChildren ? props.isExpanded : undefined}
|
expanded={hasChildren ? props.isExpanded : undefined}
|
||||||
className={`list-item-container list-item-depth-${depth} ${selected ? 'selected' : ''}`}
|
className={`list-item-container list-item-depth-${depth} ${selectionState.selected ? 'selected' : ''}`}
|
||||||
highlightOnHover={true}
|
highlightOnHover={true}
|
||||||
onDragStart={onFolderDragStart_}
|
onDragStart={onFolderDragStart_}
|
||||||
onDragOver={onFolderDragOver_}
|
onDragOver={onFolderDragOver_}
|
||||||
@@ -95,13 +96,15 @@ function FolderItem(props: FolderItemProps) {
|
|||||||
className="list-item"
|
className="list-item"
|
||||||
id={titleId}
|
id={titleId}
|
||||||
isConflictFolder={folderId === Folder.conflictFolderId()}
|
isConflictFolder={folderId === Folder.conflictFolderId()}
|
||||||
selected={selected}
|
selected={selectionState.selected}
|
||||||
shareId={shareId}
|
shareId={shareId}
|
||||||
data-folder-id={folderId}
|
data-folder-id={folderId}
|
||||||
onDoubleClick={onFolderToggleClick_}
|
onDoubleClick={onFolderToggleClick_}
|
||||||
|
|
||||||
onClick={() => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
folderItem_click(folderId);
|
folderItem_click({
|
||||||
|
id: folderId, type: ModelType.Folder, event,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{doRenderFolderIcon()}<StyledSpanFix className="title">{folderTitle}</StyledSpanFix>
|
{doRenderFolderIcon()}<StyledSpanFix className="title">{folderTitle}</StyledSpanFix>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
|
import { StyledHeader, StyledHeaderIcon, StyledHeaderLabel } from '../styles';
|
||||||
import { HeaderId, HeaderListItem } from '../types';
|
import { HeaderId, HeaderListItem } from '../types';
|
||||||
import bridge from '../../../services/bridge';
|
import bridge from '../../../services/bridge';
|
||||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
import ListItemWrapper, { ItemSelectionState, ListItemRef } from './ListItemWrapper';
|
||||||
|
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
const MenuItem = bridge().MenuItem;
|
const MenuItem = bridge().MenuItem;
|
||||||
@@ -15,7 +15,7 @@ const menuUtils = new MenuUtils(CommandService.instance());
|
|||||||
interface Props {
|
interface Props {
|
||||||
anchorRef: ListItemRef;
|
anchorRef: ListItemRef;
|
||||||
item: HeaderListItem;
|
item: HeaderListItem;
|
||||||
isSelected: boolean;
|
selectionState: ItemSelectionState;
|
||||||
onDrop: React.DragEventHandler|null;
|
onDrop: React.DragEventHandler|null;
|
||||||
index: number;
|
index: number;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
@@ -25,8 +25,6 @@ const HeaderItem: React.FC<Props> = props => {
|
|||||||
const item = props.item;
|
const item = props.item;
|
||||||
const onItemClick = item.onClick;
|
const onItemClick = item.onClick;
|
||||||
const itemId = item.id;
|
const itemId = item.id;
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const expanded = item.expanded;
|
|
||||||
|
|
||||||
const onClick: React.MouseEventHandler<HTMLElement> = useCallback(event => {
|
const onClick: React.MouseEventHandler<HTMLElement> = useCallback(event => {
|
||||||
if (onItemClick) {
|
if (onItemClick) {
|
||||||
@@ -46,18 +44,10 @@ const HeaderItem: React.FC<Props> = props => {
|
|||||||
}
|
}
|
||||||
}, [itemId]);
|
}, [itemId]);
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(() => {
|
|
||||||
setIsHovered(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
|
||||||
setIsHovered(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemWrapper
|
<ListItemWrapper
|
||||||
containerRef={props.anchorRef}
|
containerRef={props.anchorRef}
|
||||||
selected={props.isSelected}
|
selectionState={props.selectionState}
|
||||||
itemIndex={props.index}
|
itemIndex={props.index}
|
||||||
itemCount={props.itemCount}
|
itemCount={props.itemCount}
|
||||||
expanded={props.item.expanded}
|
expanded={props.item.expanded}
|
||||||
@@ -70,10 +60,8 @@ const HeaderItem: React.FC<Props> = props => {
|
|||||||
>
|
>
|
||||||
<StyledHeader
|
<StyledHeader
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
>
|
||||||
<StyledHeaderIcon aria-hidden='true' role='img' className={isHovered ? `fas ${expanded ? 'fa-caret-down' : 'fa-caret-right'}` : item.iconName}/>
|
<StyledHeaderIcon aria-hidden='true' role='img' className={item.iconName}/>
|
||||||
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
|
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
</ListItemWrapper>
|
</ListItemWrapper>
|
||||||
|
|||||||
@@ -4,9 +4,18 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
export type ListItemRef = React.Ref<HTMLDivElement>;
|
export type ListItemRef = React.Ref<HTMLDivElement>;
|
||||||
|
|
||||||
|
export interface ItemSelectionState {
|
||||||
|
selected: boolean;
|
||||||
|
// The item with primary selection is used for actions that support only one folder.
|
||||||
|
// Only one item can have primary selection.
|
||||||
|
primarySelected: boolean;
|
||||||
|
|
||||||
|
multipleItemsSelected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerRef: ListItemRef;
|
containerRef: ListItemRef;
|
||||||
selected: boolean;
|
selectionState: ItemSelectionState;
|
||||||
itemIndex: number;
|
itemIndex: number;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
expanded?: boolean|undefined;
|
expanded?: boolean|undefined;
|
||||||
@@ -35,15 +44,17 @@ const ListItemWrapper: React.FC<Props> = props => {
|
|||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
}, [props.depth]);
|
}, [props.depth]);
|
||||||
|
|
||||||
|
const { selected, primarySelected, multipleItemsSelected } = props.selectionState;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={props.containerRef}
|
ref={props.containerRef}
|
||||||
aria-posinset={props.itemIndex + 1}
|
aria-posinset={props.itemIndex + 1}
|
||||||
aria-setsize={props.itemCount}
|
aria-setsize={props.itemCount}
|
||||||
aria-selected={props.selected}
|
aria-selected={selected}
|
||||||
aria-expanded={props.expanded}
|
aria-expanded={props.expanded}
|
||||||
aria-level={props.depth}
|
aria-level={props.depth}
|
||||||
tabIndex={props.selected ? 0 : -1}
|
tabIndex={primarySelected ? 0 : -1}
|
||||||
|
|
||||||
onContextMenu={props.onContextMenu}
|
onContextMenu={props.onContextMenu}
|
||||||
onDrag={props.onDrag}
|
onDrag={props.onDrag}
|
||||||
@@ -53,10 +64,17 @@ const ListItemWrapper: React.FC<Props> = props => {
|
|||||||
draggable={props.draggable}
|
draggable={props.draggable}
|
||||||
|
|
||||||
role='treeitem'
|
role='treeitem'
|
||||||
className={`list-item-wrapper ${props.highlightOnHover ? '-highlight-on-hover' : ''} ${props.selected ? '-selected' : ''} ${props.className ?? ''}`}
|
className={[
|
||||||
|
'list-item-wrapper',
|
||||||
|
props.highlightOnHover ? '-highlight-on-hover' : '',
|
||||||
|
selected ? '-selected' : '',
|
||||||
|
primarySelected && multipleItemsSelected ? '-selected-primary' : '',
|
||||||
|
props.className ?? '',
|
||||||
|
].join(' ')}
|
||||||
style={style}
|
style={style}
|
||||||
data-folder-id={props['data-folder-id']}
|
data-folder-id={props['data-folder-id']}
|
||||||
data-id={props['data-id']}
|
data-id={props['data-id']}
|
||||||
|
data-index={props.itemIndex}
|
||||||
data-tag-id={props['data-tag-id']}
|
data-tag-id={props['data-tag-id']}
|
||||||
data-type={props['data-type']}
|
data-type={props['data-type']}
|
||||||
aria-labelledby={props['aria-labelledby']}
|
aria-labelledby={props['aria-labelledby']}
|
||||||
|
|||||||
@@ -3,28 +3,27 @@ import * as React from 'react';
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { StyledListItemAnchor, StyledSpanFix } from '../styles';
|
import { StyledListItemAnchor, StyledSpanFix } from '../styles';
|
||||||
import { TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
import { TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
||||||
import BaseModel from '@joplin/lib/BaseModel';
|
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||||
import NoteCount from './NoteCount';
|
import NoteCount from './NoteCount';
|
||||||
import EmptyExpandLink from './EmptyExpandLink';
|
import EmptyExpandLink from './EmptyExpandLink';
|
||||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
import ListItemWrapper, { ItemSelectionState, ListItemRef } from './ListItemWrapper';
|
||||||
|
import { ItemClickEvent } from '../hooks/useOnItemClick';
|
||||||
export type TagLinkClickEvent = { tag: TagsWithNoteCountEntity|undefined };
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
anchorRef: ListItemRef;
|
anchorRef: ListItemRef;
|
||||||
selected: boolean;
|
selectionState: ItemSelectionState;
|
||||||
tag: TagsWithNoteCountEntity;
|
tag: TagsWithNoteCountEntity;
|
||||||
label: string;
|
label: string;
|
||||||
onTagDrop: React.DragEventHandler<HTMLElement>;
|
onTagDrop: React.DragEventHandler<HTMLElement>;
|
||||||
onContextMenu: React.MouseEventHandler<HTMLElement>;
|
onContextMenu: React.MouseEventHandler<HTMLElement>;
|
||||||
onClick: (event: TagLinkClickEvent)=> void;
|
onClick: (event: ItemClickEvent)=> void;
|
||||||
|
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TagItem = (props: Props) => {
|
const TagItem = (props: Props) => {
|
||||||
const { tag, selected } = props;
|
const { tag, selectionState } = props;
|
||||||
|
|
||||||
let noteCount = null;
|
let noteCount = null;
|
||||||
if (Setting.value('showNoteCounts')) {
|
if (Setting.value('showNoteCounts')) {
|
||||||
@@ -32,30 +31,31 @@ const TagItem = (props: Props) => {
|
|||||||
noteCount = <NoteCount count={count}/>;
|
noteCount = <NoteCount count={count}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickHandler = useCallback(() => {
|
const onClickHandler: React.MouseEventHandler<HTMLElement> = useCallback((event) => {
|
||||||
props.onClick({ tag });
|
props.onClick({ id: tag.id, type: ModelType.Tag, event });
|
||||||
}, [props.onClick, tag]);
|
}, [props.onClick, tag]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemWrapper
|
<ListItemWrapper
|
||||||
containerRef={props.anchorRef}
|
containerRef={props.anchorRef}
|
||||||
selected={selected}
|
selectionState={selectionState}
|
||||||
depth={1}
|
depth={1}
|
||||||
className={`list-item-container ${selected ? 'selected' : ''}`}
|
className={`list-item-container ${selectionState.selected ? 'selected' : ''}`}
|
||||||
highlightOnHover={true}
|
highlightOnHover={true}
|
||||||
onDrop={props.onTagDrop}
|
onDrop={props.onTagDrop}
|
||||||
|
onContextMenu={props.onContextMenu}
|
||||||
|
data-id={tag.id}
|
||||||
data-tag-id={tag.id}
|
data-tag-id={tag.id}
|
||||||
aria-selected={selected}
|
data-type={ModelType.Tag}
|
||||||
itemIndex={props.index}
|
itemIndex={props.index}
|
||||||
itemCount={props.itemCount}
|
itemCount={props.itemCount}
|
||||||
>
|
>
|
||||||
<EmptyExpandLink/>
|
<EmptyExpandLink/>
|
||||||
<StyledListItemAnchor
|
<StyledListItemAnchor
|
||||||
className="list-item"
|
className="list-item"
|
||||||
selected={selected}
|
selected={selectionState.selected}
|
||||||
data-id={tag.id}
|
data-id={tag.id}
|
||||||
data-type={BaseModel.TYPE_TAG}
|
data-type={BaseModel.TYPE_TAG}
|
||||||
onContextMenu={props.onContextMenu}
|
|
||||||
onClick={onClickHandler}
|
onClick={onClickHandler}
|
||||||
>
|
>
|
||||||
<StyledSpanFix className="tag-label">{props.label}</StyledSpanFix>
|
<StyledSpanFix className="tag-label">{props.label}</StyledSpanFix>
|
||||||
|
|||||||
@@ -22,7 +22,30 @@
|
|||||||
background: var(--joplin-selected-color2);
|
background: var(--joplin-selected-color2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-highlight-on-hover:hover {
|
// When multiple items are selected, show an outline (similar to the focus outline) to indicate
|
||||||
|
// which folder has the primary selection.
|
||||||
|
&.-selected-primary {
|
||||||
|
--outline-color: var(--joplin-focus-outline-color-dimmed);
|
||||||
|
outline: 1px solid var(--outline-color);
|
||||||
|
|
||||||
|
// Also adjust the background color: This makes it clearer which item has primary focus,
|
||||||
|
// especially when using a dimmed outline.
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--outline-color) 12%,
|
||||||
|
var(--joplin-selected-color2) 92%
|
||||||
|
);
|
||||||
|
|
||||||
|
// For accessibility, use a different style when actually focused. This makes it easier to
|
||||||
|
// tell where the keyboard focus is.
|
||||||
|
&:focus {
|
||||||
|
--outline-color: var(--joplin-focus-outline-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't highlight selected items on hover -- doing so makes it
|
||||||
|
// difficult to tell whether the hovered item is selected or not.
|
||||||
|
&.-highlight-on-hover:not(.-selected):hover {
|
||||||
background-color: var(--joplin-background-color-hover2);
|
background-color: var(--joplin-background-color-hover2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,8 +57,10 @@ export interface SpacerListItem extends ToplevelListItem {
|
|||||||
|
|
||||||
export type ListItem = HeaderListItem|AllNotesListItem|TagListItem|FolderListItem|SpacerListItem;
|
export type ListItem = HeaderListItem|AllNotesListItem|TagListItem|FolderListItem|SpacerListItem;
|
||||||
|
|
||||||
|
interface SetSelectedIndexOptions {
|
||||||
export type SetSelectedIndexCallback = (newIndex: number)=> void;
|
extend: boolean;
|
||||||
|
}
|
||||||
|
export type SetSelectedIndexCallback = (newIndex: number, options: SetSelectedIndexOptions)=> void;
|
||||||
|
|
||||||
|
|
||||||
export type ItemDragListener = DragEventHandler<HTMLElement>;
|
export type ItemDragListener = DragEventHandler<HTMLElement>;
|
||||||
|
|||||||
@@ -40,12 +40,18 @@ async function exportDebugReportClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StatusScreen(props: Props) {
|
function StatusScreen(props: Props) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [report, setReport] = useState<ReportSection[]>([]);
|
const [report, setReport] = useState<ReportSection[]>([]);
|
||||||
|
|
||||||
async function refreshScreen() {
|
async function refreshScreen() {
|
||||||
const service = new ReportService();
|
setLoading(true);
|
||||||
const r = await service.status(Setting.value('sync.target'));
|
try {
|
||||||
setReport(r);
|
const service = new ReportService();
|
||||||
|
const r = await service.status(Setting.value('sync.target'));
|
||||||
|
setReport(r);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -208,6 +214,7 @@ function StatusScreen(props: Props) {
|
|||||||
<div style={style}>
|
<div style={style}>
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
{renderTools()}
|
{renderTools()}
|
||||||
|
{loading && <p><span className='loading-animation'/> {_('Loading...')}</p>}
|
||||||
{body}
|
{body}
|
||||||
</div>
|
</div>
|
||||||
<ButtonBar
|
<ButtonBar
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const ModalMessageOverlay: React.FC<Props> = ({ message }) => {
|
|||||||
|
|
||||||
return <Dialog contentFillsScreen={true}>
|
return <Dialog contentFillsScreen={true}>
|
||||||
<div className="modal-message">
|
<div className="modal-message">
|
||||||
<div id="loading-animation" />
|
<div className="loading-animation" />
|
||||||
<div className="text" role="status">
|
<div className="text" role="status">
|
||||||
{lines}
|
{lines}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import bridge from '../../../services/bridge';
|
import bridge from '../../../services/bridge';
|
||||||
import Folder from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
import { getTrashFolderId } from '@joplin/lib/services/trash';
|
||||||
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
|
||||||
|
|
||||||
export const declaration: CommandDeclaration = {
|
export const declaration: CommandDeclaration = {
|
||||||
@@ -11,22 +12,37 @@ export const declaration: CommandDeclaration = {
|
|||||||
|
|
||||||
export const runtime = (): CommandRuntime => {
|
export const runtime = (): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
execute: async (context: CommandContext, folderId: string = null) => {
|
execute: async (context: CommandContext, folderIds: string|string[] = null) => {
|
||||||
if (folderId === null) folderId = context.state.selectedFolderId;
|
if (folderIds === null) {
|
||||||
|
folderIds = context.state.selectedFolderIds;
|
||||||
const folder = await Folder.load(folderId);
|
}
|
||||||
if (!folder) throw new Error(`No such folder: ${folderId}`);
|
if (!Array.isArray(folderIds)) {
|
||||||
|
folderIds = [folderIds];
|
||||||
let deleteMessage = _('Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.', substrWithEllipsis(folder.title, 0, 32));
|
|
||||||
if (folderId === context.state.settings['sync.10.inboxId']) {
|
|
||||||
deleteMessage = _('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = bridge().showConfirmMessageBox(deleteMessage);
|
folderIds = folderIds.filter(id => id !== getTrashFolderId());
|
||||||
|
if (folderIds.length === 0) {
|
||||||
|
throw new Error('Nothing to do: At least one valid folder must be specified.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await Folder.loadItemsByIdsOrFail(folderIds);
|
||||||
|
|
||||||
|
const deleteMessage = [];
|
||||||
|
if (folders.length === 1) {
|
||||||
|
deleteMessage.push(_('Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.', substrWithEllipsis(folders[0].title, 0, 32)));
|
||||||
|
} else {
|
||||||
|
deleteMessage.push(_('Move %d notebooks to the trash?\n\nAll notes and sub-notebooks within these notebooks will also be moved to the trash.', folders.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folders.some(folder => folder.id === context.state.settings['sync.10.inboxId'])) {
|
||||||
|
deleteMessage.push(_('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = bridge().showConfirmMessageBox(deleteMessage.join('\n\n'));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
await Folder.delete(folderId, { toTrash: true, sourceDescription: 'deleteFolder command' });
|
await Folder.batchDelete(folderIds, { toTrash: true, sourceDescription: 'deleteFolder command' });
|
||||||
},
|
},
|
||||||
enabledCondition: '!folderIsReadOnly',
|
enabledCondition: '!foldersIncludeReadOnly',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||||
|
import { FileSystemItem, ImportModuleOutputFormat, ModuleType } from '@joplin/lib/services/interop/types';
|
||||||
|
import bridge from '../../../services/bridge';
|
||||||
|
import { WindowControl } from '../utils/useWindowControl';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
|
||||||
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import { PackageInfo } from '@joplin/lib/versionInfo';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
import { ImportModule } from '@joplin/lib/services/interop/Module';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
const packageInfo: PackageInfo = require('../../../packageInfo.js');
|
||||||
|
|
||||||
|
const logger = Logger.create('importFrom');
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'importFrom',
|
||||||
|
label: () => _('Import...'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ImportCommandOptions {
|
||||||
|
sourcePath: string|undefined;
|
||||||
|
sourceType: FileSystemItem;
|
||||||
|
destinationFolderId: string|null;
|
||||||
|
importFormat: string;
|
||||||
|
outputFormat: ImportModuleOutputFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findImportModule = async (commandOptions: ImportCommandOptions|null, control: WindowControl) => {
|
||||||
|
if (commandOptions) {
|
||||||
|
const module = InteropService.instance().findModuleByFormat(
|
||||||
|
ModuleType.Importer, commandOptions.importFormat, commandOptions.sourceType, commandOptions.outputFormat);
|
||||||
|
if (module) {
|
||||||
|
return module as ImportModule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importModules = InteropService.instance().modules().filter(module => module.type === ModuleType.Importer) as ImportModule[];
|
||||||
|
return await control.showPrompt({
|
||||||
|
label: _('Select the type of file to be imported:'),
|
||||||
|
value: '',
|
||||||
|
suggestions: importModules.map(module => {
|
||||||
|
const label = module.fullLabel();
|
||||||
|
return {
|
||||||
|
key: `${module.type}--${label}`,
|
||||||
|
value: module,
|
||||||
|
label: module.fullLabel(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const promptForSourcePath = async (module: ImportModule, sourceType: FileSystemItem|undefined) => {
|
||||||
|
if (!sourceType) {
|
||||||
|
if (!module.sources.includes(FileSystemItem.Directory)) {
|
||||||
|
sourceType = FileSystemItem.File;
|
||||||
|
}
|
||||||
|
if (!module.sources.includes(FileSystemItem.File)) {
|
||||||
|
sourceType = FileSystemItem.Directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sourceType === FileSystemItem.File) {
|
||||||
|
return await bridge().showOpenDialog({
|
||||||
|
filters: [{ name: module.description, extensions: module.fileExtensions }],
|
||||||
|
});
|
||||||
|
} else if (sourceType === FileSystemItem.Directory) {
|
||||||
|
return await bridge().showOpenDialog({
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return await bridge().showOpenDialog({
|
||||||
|
properties: ['openDirectory', 'openFile'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (control: WindowControl): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
// Since this can be run from "go to anything", partialOptions needs to support being null or empty.
|
||||||
|
execute: async (context: CommandContext, options: ImportCommandOptions|undefined) => {
|
||||||
|
const importModule = await findImportModule(options, control);
|
||||||
|
if (!importModule) return null; // E.g. if cancelled
|
||||||
|
|
||||||
|
let sourcePath = options?.sourcePath ?? await promptForSourcePath(importModule, options?.sourceType);
|
||||||
|
if (Array.isArray(sourcePath)) {
|
||||||
|
sourcePath = sourcePath[0];
|
||||||
|
}
|
||||||
|
// Handle the case where the directory picker action was cancelled
|
||||||
|
if (!sourcePath) return null;
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
const isDirectory = await shim.fsDriver().isDirectory(sourcePath);
|
||||||
|
const importsMultipleNotes = importModule.isNoteArchive || isDirectory;
|
||||||
|
|
||||||
|
const destinationFolderId = importsMultipleNotes ? null : context.state.selectedFolderId;
|
||||||
|
const importFormat = importModule.format;
|
||||||
|
const outputFormat = importModule.outputFormat;
|
||||||
|
options = {
|
||||||
|
sourcePath,
|
||||||
|
destinationFolderId,
|
||||||
|
importFormat,
|
||||||
|
outputFormat,
|
||||||
|
sourceType: isDirectory ? FileSystemItem.Directory : FileSystemItem.File,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalMessage = _('Importing from "%s" as "%s" format. Please wait...', sourcePath, options.importFormat);
|
||||||
|
void CommandService.instance().execute('showModalMessage', modalMessage);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
const errors: any[] = [];
|
||||||
|
|
||||||
|
const importOptions = {
|
||||||
|
path: sourcePath,
|
||||||
|
format: options.importFormat,
|
||||||
|
outputFormat: options.outputFormat,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
onProgress: (status: any) => {
|
||||||
|
const statusStrings: string[] = Object.keys(status).map((key: string) => {
|
||||||
|
return `${key}: ${status[key]}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
onError: (error: any) => {
|
||||||
|
errors.push(error);
|
||||||
|
console.warn(error);
|
||||||
|
},
|
||||||
|
destinationFolderId: options.destinationFolderId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = InteropService.instance();
|
||||||
|
try {
|
||||||
|
const result = await service.import(importOptions);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.info('Import result: ', result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
bridge().showErrorMessageBox(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandService.instance().execute('hideModalMessage');
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
const response = bridge().showErrorMessageBox('There were some errors importing the notes - check the console for more details.\n\nPlease consider sending a bug report to the forum!', {
|
||||||
|
buttons: [_('Close'), _('Send bug report')],
|
||||||
|
});
|
||||||
|
|
||||||
|
context.dispatch({ type: 'NOTE_DEVTOOLS_SET', value: true });
|
||||||
|
|
||||||
|
if (response === 1) {
|
||||||
|
const url = makeDiscourseDebugUrl(
|
||||||
|
`Error importing notes from format: ${options.importFormat}`,
|
||||||
|
`- Input format: ${options.importFormat}\n- Output format: ${options.outputFormat}`,
|
||||||
|
errors,
|
||||||
|
packageInfo,
|
||||||
|
PluginService.instance(),
|
||||||
|
Setting.value('plugins.states'),
|
||||||
|
);
|
||||||
|
|
||||||
|
void bridge().openExternal(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabledCondition: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import * as editAlarm from './editAlarm';
|
|||||||
import * as exportPdf from './exportPdf';
|
import * as exportPdf from './exportPdf';
|
||||||
import * as gotoAnything from './gotoAnything';
|
import * as gotoAnything from './gotoAnything';
|
||||||
import * as hideModalMessage from './hideModalMessage';
|
import * as hideModalMessage from './hideModalMessage';
|
||||||
|
import * as importFrom from './importFrom';
|
||||||
import * as linkToNote from './linkToNote';
|
import * as linkToNote from './linkToNote';
|
||||||
import * as moveToFolder from './moveToFolder';
|
import * as moveToFolder from './moveToFolder';
|
||||||
import * as newFolder from './newFolder';
|
import * as newFolder from './newFolder';
|
||||||
@@ -55,6 +56,7 @@ const index: any[] = [
|
|||||||
exportPdf,
|
exportPdf,
|
||||||
gotoAnything,
|
gotoAnything,
|
||||||
hideModalMessage,
|
hideModalMessage,
|
||||||
|
importFrom,
|
||||||
linkToNote,
|
linkToNote,
|
||||||
moveToFolder,
|
moveToFolder,
|
||||||
newFolder,
|
newFolder,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||||
import { ModelType } from '@joplin/lib/BaseModel';
|
import { ModelType } from '@joplin/lib/BaseModel';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
|
import showFolderPicker from '../utils/showFolderPicker';
|
||||||
|
|
||||||
const logger = Logger.create('commands/moveToFolder');
|
const logger = Logger.create('commands/moveToFolder');
|
||||||
|
|
||||||
@@ -31,71 +32,37 @@ export const runtime = (comp: any): CommandRuntime => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const folders = await Folder.sortFolderTree();
|
const targetFolderId = await showFolderPicker(comp, {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
label: _('Move to notebook:'),
|
||||||
const startFolders: any[] = [];
|
// It's okay for folders (but not notes) to have no parent folder:
|
||||||
const maxDepth = 15;
|
allowSelectNone: allAreFolders,
|
||||||
|
// Don't allow setting a folder as its own parent
|
||||||
// It's okay for folders (but not notes) to have no parent folder:
|
showFolder: (folder) => !itemIdToType.has(folder.id),
|
||||||
if (allAreFolders) {
|
|
||||||
startFolders.push({
|
|
||||||
key: '',
|
|
||||||
value: '',
|
|
||||||
label: _('None'),
|
|
||||||
indentDepth: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const addOptions = (folders: FolderEntityWithChildren[], depth: number) => {
|
|
||||||
for (let i = 0; i < folders.length; i++) {
|
|
||||||
const folder = folders[i];
|
|
||||||
|
|
||||||
// Disallow making a folder a subfolder of itself.
|
|
||||||
if (itemIdToType.has(folder.id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth });
|
|
||||||
if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
addOptions(folders, 0);
|
|
||||||
|
|
||||||
comp.setState({
|
|
||||||
promptOptions: {
|
|
||||||
label: _('Move to notebook:'),
|
|
||||||
inputType: 'dropdown',
|
|
||||||
value: '',
|
|
||||||
autocomplete: startFolders,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onClose: async (answer: any) => {
|
|
||||||
if (answer) {
|
|
||||||
try {
|
|
||||||
const targetFolderId = answer.value;
|
|
||||||
for (const id of itemIds) {
|
|
||||||
if (id === targetFolderId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemType = itemIdToType.get(id);
|
|
||||||
if (itemType === ModelType.Note) {
|
|
||||||
await Note.moveToFolder(id, targetFolderId);
|
|
||||||
} else if (itemType === ModelType.Folder) {
|
|
||||||
await Folder.moveToFolder(id, targetFolderId);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Cannot move item with type ${itemType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error moving items', error);
|
|
||||||
void shim.showMessageBox(`Error: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
comp.setState({ promptOptions: null });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// It's important to allow the case where targetFolderId is the empty string,
|
||||||
|
// since that corresponds to the toplevel notebook.
|
||||||
|
if (targetFolderId !== null) {
|
||||||
|
try {
|
||||||
|
for (const id of itemIds) {
|
||||||
|
if (id === targetFolderId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemType = itemIdToType.get(id);
|
||||||
|
if (itemType === ModelType.Note) {
|
||||||
|
await Note.moveToFolder(id, targetFolderId);
|
||||||
|
} else if (itemType === ModelType.Folder) {
|
||||||
|
await Folder.moveToFolder(id, targetFolderId);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Cannot move item with type ${itemType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error moving items', error);
|
||||||
|
void shim.showMessageBox(`Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
enabledCondition: 'someNotesSelected && !noteIsReadOnly',
|
enabledCondition: 'someNotesSelected && !noteIsReadOnly',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
|
||||||
export const newNoteEnabledConditions = 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly && !folderIsTrash';
|
export const newNoteEnabledConditions = 'oneFolderSelected && !inConflictFolder && !folderIsReadOnly && !folderIsTrash';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export const declaration: CommandDeclaration = {
|
|||||||
export const runtime = (): CommandRuntime => {
|
export const runtime = (): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
execute: async (_context: CommandContext, body = '', isTodo = false) => {
|
execute: async (_context: CommandContext, body = '', isTodo = false) => {
|
||||||
const folderId = Setting.value('activeFolderId');
|
const folderId = await Folder.getValidActiveFolder();
|
||||||
if (!folderId) return;
|
if (!folderId) return;
|
||||||
|
|
||||||
const defaultValues = Note.previewFieldsWithDefaultValues({ includeTimestamps: false });
|
const defaultValues = Note.previewFieldsWithDefaultValues({ includeTimestamps: false });
|
||||||
|
|||||||