You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
179 Commits
android-v3
...
android-v3
Author | SHA1 | Date | |
---|---|---|---|
|
a2069df3e0 | ||
|
1ad150c1bf | ||
|
41b251d67a | ||
|
2c40cec639 | ||
|
efb58c5f40 | ||
|
9d8cd1d707 | ||
|
591c458a4f | ||
|
f9b1a32ae7 | ||
|
1a195e23dd | ||
|
26ae3f853e | ||
|
e84e9a58e1 | ||
|
3b8da5023d | ||
|
548d41d0d4 | ||
|
d6c921249f | ||
|
e044c50b03 | ||
|
beec74d792 | ||
|
8b4e163b28 | ||
|
b61467097d | ||
|
447e4638d1 | ||
|
b831525b20 | ||
|
e05be832d5 | ||
|
64c9c3179f | ||
|
0ea61f26eb | ||
|
349fa426ea | ||
|
e3d5f0c9cf | ||
|
e63d545ed8 | ||
|
ab3058612d | ||
|
715abcce32 | ||
|
f165b3f870 | ||
|
8895d745e7 | ||
|
33a9b96a31 | ||
|
d1ac3d415e | ||
|
432fac8fda | ||
|
0f23882d47 | ||
|
693c0f22c8 | ||
|
e2db7a6b61 | ||
|
2a74f60812 | ||
|
2419291976 | ||
|
733845eb95 | ||
|
b3315aeb03 | ||
|
d88c522d96 | ||
|
c0cefc30f4 | ||
|
0dc3589661 | ||
|
f64c3d5484 | ||
|
5fceb5a3c9 | ||
|
916b3f6f69 | ||
|
0c4e8eeafc | ||
|
b27e0ff1f4 | ||
|
59ffb0f265 | ||
|
20b4fd85c1 | ||
|
fc2da05ba6 | ||
|
948ca605b0 | ||
|
eda2c69334 | ||
|
42ab9ecd95 | ||
|
5935c9c147 | ||
|
90640e590e | ||
|
75b8caf816 | ||
|
3ea403d004 | ||
|
058a559de4 | ||
|
ac43c62ce8 | ||
|
c4a7749f2a | ||
|
e6c09da639 | ||
|
2d05b5f43e | ||
|
63d0855a59 | ||
|
3d42485315 | ||
|
f772cc500c | ||
|
ad8bcacbca | ||
|
fbab549a1c | ||
|
817f3bc121 | ||
|
e3576683b0 | ||
|
85c2eb43dd | ||
|
0f2b2b1e7b | ||
|
8fd2eeaea5 | ||
|
b97a14c559 | ||
|
bbb97bcb02 | ||
|
8a51ed892a | ||
|
0cac69c2fa | ||
|
feb946acfb | ||
|
220f867814 | ||
|
050a896c8b | ||
|
d13e7b32c3 | ||
|
a56f104fe8 | ||
|
99696637b9 | ||
|
be5a6c189a | ||
|
a01f519131 | ||
|
a71ee1d0b8 | ||
|
a40bb77feb | ||
|
5c23765458 | ||
|
d023ce592c | ||
|
8c4bf057d6 | ||
|
b9dc226031 | ||
|
a81c1ff663 | ||
|
c909d85acc | ||
|
0965c6d257 | ||
|
5beb80bf61 | ||
|
1b2f5e5cd8 | ||
|
2db82ac732 | ||
|
3f1ec682b9 | ||
|
59b3030e45 | ||
|
54d223a721 | ||
|
e5771a36bb | ||
|
31a5ee20df | ||
|
efd9ada977 | ||
|
b4450ae4ef | ||
|
73076bd4b7 | ||
|
0ba0550baf | ||
|
41b03f9356 | ||
|
95f1992b8a | ||
|
11c1c0638d | ||
|
e0daf807a6 | ||
|
2594c1edb1 | ||
|
e80bede7b7 | ||
|
1eb721c717 | ||
|
38b6484f12 | ||
|
a0163ba793 | ||
|
e2e589e907 | ||
|
93f96c03b1 | ||
|
77f09a4408 | ||
|
faf30306da | ||
|
c1c02204fa | ||
|
017480eb45 | ||
|
8931a68ec8 | ||
|
3c6a419cad | ||
|
dce4c715e3 | ||
|
5763de3b26 | ||
|
4fa61e443f | ||
|
84e312563a | ||
|
707c21a2fe | ||
|
d0057ae838 | ||
|
8d3ac630c5 | ||
|
b5f06b6958 | ||
|
5a07b795d3 | ||
|
bfab4426ca | ||
|
bcb5218e1a | ||
|
c897cc1582 | ||
|
ea61bfc498 | ||
|
ca5d35339f | ||
|
5c00ea93c2 | ||
|
f005977ce0 | ||
|
79773dab95 | ||
|
69168f1ec2 | ||
|
147a66d64e | ||
|
ec36847de0 | ||
|
d7bef7e923 | ||
|
55faab25b5 | ||
|
4da8060e62 | ||
|
821cfc5bd8 | ||
|
9956caea1b | ||
|
f95b663f28 | ||
|
dd990e7cf6 | ||
|
23dec124dd | ||
|
2b6cb908fa | ||
|
40475d60fb | ||
|
48e96a055f | ||
|
3dbc9a5723 | ||
|
9832af0d3a | ||
|
26caf2a4c6 | ||
|
29d7804ffd | ||
|
6fe0104483 | ||
|
04f5433839 | ||
|
0bfa28d795 | ||
|
ac2258769a | ||
|
7cd0ed1714 | ||
|
803d508c69 | ||
|
3c13568107 | ||
|
e41394b57f | ||
|
0b13dbddd8 | ||
|
2a2dd96c02 | ||
|
2f7b2fb948 | ||
|
4e8316a6ee | ||
|
01412b4500 | ||
|
2df8137281 | ||
|
f24e229a4e | ||
|
fa6060d6d2 | ||
|
2d6796db16 | ||
|
82be4f566a | ||
|
f353686166 | ||
|
a6dbe4b67a | ||
|
b597d5f9d1 |
@@ -209,7 +209,6 @@ packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/deleteNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
|
||||
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
|
||||
@@ -228,7 +227,6 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/print.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameTag.js
|
||||
@@ -287,6 +285,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -375,6 +374,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||
@@ -442,7 +442,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
|
||||
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
@@ -476,6 +475,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/pluginApi.spec.js
|
||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||
packages/app-desktop/integration-tests/settings.spec.js
|
||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
@@ -517,8 +517,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
|
||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/copy7Zip.js
|
||||
packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
packages/app-desktop/tools/modifyReleaseAssets.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/tools/renameReleaseAssets.js
|
||||
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
|
||||
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
@@ -545,6 +547,7 @@ packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
@@ -608,10 +611,12 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteItem.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader/Menu.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBox.js
|
||||
@@ -619,6 +624,7 @@ packages/app-mobile/components/ScreenHeader/WebBetaButton.js
|
||||
packages/app-mobile/components/ScreenHeader/index.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
@@ -707,13 +713,14 @@ packages/app-mobile/components/screens/Note.test.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.test.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/search.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
@@ -723,6 +730,7 @@ packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/BackButtonService.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
@@ -757,6 +765,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
@@ -778,6 +787,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
|
||||
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/default-plugins/build.js
|
||||
@@ -883,6 +893,7 @@ packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.test.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
packages/lib/BaseApplication.js
|
||||
packages/lib/BaseModel.test.js
|
||||
packages/lib/BaseModel.js
|
||||
packages/lib/BaseSyncTarget.js
|
||||
packages/lib/ClipperServer.js
|
||||
@@ -912,11 +923,14 @@ packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/deleteNote.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
@@ -942,6 +956,7 @@ packages/lib/eventManager.js
|
||||
packages/lib/file-api-driver-joplinServer.js
|
||||
packages/lib/file-api-driver-local.js
|
||||
packages/lib/file-api-driver-memory.js
|
||||
packages/lib/file-api-driver-webdav.test.js
|
||||
packages/lib/file-api-driver.test.js
|
||||
packages/lib/file-api.test.js
|
||||
packages/lib/file-api.js
|
||||
@@ -955,6 +970,8 @@ packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.test.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils2.test.js
|
||||
|
13
.github/workflows/build-macos-m1.yml
vendored
13
.github/workflows/build-macos-m1.yml
vendored
@@ -44,6 +44,14 @@ jobs:
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Set Publish Flag
|
||||
run: |
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
echo "PUBLISH_ENABLED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "PUBLISH_ENABLED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Build macOS M1 app
|
||||
env:
|
||||
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
|
||||
@@ -56,6 +64,7 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
PUBLISH_ENABLED: ${{ env.PUBLISH_ENABLED }}
|
||||
run: |
|
||||
export npm_config_arch=arm64
|
||||
export npm_config_target_arch=arm64
|
||||
@@ -67,11 +76,11 @@ jobs:
|
||||
npm pkg set 'build.mac.target[1].target'='zip'
|
||||
npm pkg set 'build.mac.target[1].arch[0]'='arm64'
|
||||
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
|
||||
echo "Building and publishing desktop application..."
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
|
||||
|
||||
yarn renameReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
|
||||
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
|
||||
else
|
||||
echo "Building but *not* publishing desktop application..."
|
||||
|
||||
|
27
.gitignore
vendored
27
.gitignore
vendored
@@ -186,7 +186,6 @@ packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/deleteNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
|
||||
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
|
||||
@@ -205,7 +204,6 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/print.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
|
||||
packages/app-desktop/gui/MainScreen/commands/renameTag.js
|
||||
@@ -264,6 +262,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -352,6 +351,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||
@@ -419,7 +419,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
|
||||
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarBase.js
|
||||
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
|
||||
packages/app-desktop/gui/ToolbarButton/styles/index.js
|
||||
packages/app-desktop/gui/ToolbarSpace.js
|
||||
packages/app-desktop/gui/TrashNotification/TrashNotification.js
|
||||
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
|
||||
@@ -453,6 +452,7 @@ packages/app-desktop/integration-tests/models/NoteList.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/pluginApi.spec.js
|
||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||
packages/app-desktop/integration-tests/settings.spec.js
|
||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||
@@ -494,8 +494,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
|
||||
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
|
||||
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/copy7Zip.js
|
||||
packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
packages/app-desktop/tools/modifyReleaseAssets.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/tools/renameReleaseAssets.js
|
||||
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
|
||||
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
@@ -522,6 +524,7 @@ packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/Checkbox.js
|
||||
packages/app-mobile/components/DialogManager.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
@@ -585,10 +588,12 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
|
||||
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteItem.js
|
||||
packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ScreenHeader/Menu.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBox.js
|
||||
@@ -596,6 +601,7 @@ packages/app-mobile/components/ScreenHeader/WebBetaButton.js
|
||||
packages/app-mobile/components/ScreenHeader/index.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenuContentNote.js
|
||||
packages/app-mobile/components/TextInput.js
|
||||
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
|
||||
packages/app-mobile/components/accessibility/AccessibleView.js
|
||||
@@ -684,13 +690,14 @@ packages/app-mobile/components/screens/Note.test.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.test.js
|
||||
packages/app-mobile/components/screens/ShareManager/index.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/search.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
|
||||
@@ -700,6 +707,7 @@ packages/app-mobile/root.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.android.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.ios.js
|
||||
packages/app-mobile/services/AlarmServiceDriver.web.js
|
||||
packages/app-mobile/services/BackButtonService.js
|
||||
packages/app-mobile/services/e2ee/RSA.react-native.js
|
||||
packages/app-mobile/services/plugins/PlatformImplementation.js
|
||||
packages/app-mobile/services/profiles/index.js
|
||||
@@ -734,6 +742,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
|
||||
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
|
||||
packages/app-mobile/utils/getPackageInfo.js
|
||||
packages/app-mobile/utils/getVersionInfoText.js
|
||||
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
|
||||
packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
@@ -755,6 +764,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
|
||||
packages/app-mobile/utils/shim-init-react/shimInitShared.js
|
||||
packages/app-mobile/utils/testing/createMockReduxStore.js
|
||||
packages/app-mobile/utils/testing/getWebViewDomById.js
|
||||
packages/app-mobile/utils/testing/getWebViewWindowById.js
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/web/serviceWorker.js
|
||||
packages/default-plugins/build.js
|
||||
@@ -860,6 +870,7 @@ packages/lib/ArrayUtils.js
|
||||
packages/lib/AsyncActionQueue.test.js
|
||||
packages/lib/AsyncActionQueue.js
|
||||
packages/lib/BaseApplication.js
|
||||
packages/lib/BaseModel.test.js
|
||||
packages/lib/BaseModel.js
|
||||
packages/lib/BaseSyncTarget.js
|
||||
packages/lib/ClipperServer.js
|
||||
@@ -889,11 +900,14 @@ packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/deleteNote.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
packages/lib/components/shared/config/config-shared.js
|
||||
@@ -919,6 +933,7 @@ packages/lib/eventManager.js
|
||||
packages/lib/file-api-driver-joplinServer.js
|
||||
packages/lib/file-api-driver-local.js
|
||||
packages/lib/file-api-driver-memory.js
|
||||
packages/lib/file-api-driver-webdav.test.js
|
||||
packages/lib/file-api-driver.test.js
|
||||
packages/lib/file-api.test.js
|
||||
packages/lib/file-api.js
|
||||
@@ -932,6 +947,8 @@ packages/lib/hooks/useElementSize.js
|
||||
packages/lib/hooks/useEventListener.js
|
||||
packages/lib/hooks/usePlugin.js
|
||||
packages/lib/hooks/usePrevious.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.test.js
|
||||
packages/lib/hooks/useQueuedAsyncEffect.js
|
||||
packages/lib/htmlUtils.test.js
|
||||
packages/lib/htmlUtils.js
|
||||
packages/lib/htmlUtils2.test.js
|
||||
|
874
.yarn/releases/yarn-3.6.4.cjs
vendored
874
.yarn/releases/yarn-3.6.4.cjs
vendored
File diff suppressed because one or more lines are too long
875
.yarn/releases/yarn-3.8.3.cjs
vendored
Executable file
875
.yarn/releases/yarn-3.8.3.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@ plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.6.4.cjs
|
||||
yarnPath: .yarn/releases/yarn-3.8.3.cjs
|
||||
|
||||
logFilters:
|
||||
|
||||
|
BIN
Assets/WebsiteAssets/images/sponsors/CasinoReviews.png
Normal file
BIN
Assets/WebsiteAssets/images/sponsors/CasinoReviews.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
|
||||
# Sponsors
|
||||
|
||||
<!-- 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://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.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://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></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://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.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://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a>
|
||||
<!-- SPONSORS-ORG -->
|
||||
|
||||
* * *
|
||||
|
28
package.json
28
package.json
@@ -72,38 +72,38 @@
|
||||
"@crowdin/cli": "3",
|
||||
"@joplin/utils": "~2.12",
|
||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.8.0",
|
||||
"@typescript-eslint/parser": "6.8.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"cspell": "5.21.2",
|
||||
"eslint": "8.52.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-interactive": "10.8.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-jest": "27.4.3",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jest": "27.9.0",
|
||||
"eslint-plugin-promise": "6.2.0",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.3.16",
|
||||
"glob": "10.4.5",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "15.2.5",
|
||||
"lint-staged": "15.2.7",
|
||||
"madge": "6.1.0",
|
||||
"npm-package-json-lint": "7.1.0",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"eslint-plugin-github": "4.10.1",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "9.4.1",
|
||||
"nodemon": "3.0.3"
|
||||
"nodemon": "3.1.7"
|
||||
},
|
||||
"packageManager": "yarn@3.6.4",
|
||||
"packageManager": "yarn@3.8.3",
|
||||
"resolutions": {
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
|
||||
"eslint": "patch:eslint@8.52.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"eslint": "patch:eslint@8.57.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
|
||||
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env -S NODE_OPTIONS=--no-deprecation node
|
||||
|
||||
// Use njstrace to find out what Node.js might be spending time on
|
||||
// var njstrace = require('njstrace').inject();
|
||||
|
@@ -72,12 +72,12 @@
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.1",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.34",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.19.39",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"temp": "0.9.4",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import MdToHtml from '@joplin/renderer/MdToHtml';
|
||||
import MdToHtml, { LinkRenderingType } from '@joplin/renderer/MdToHtml';
|
||||
const { filename } = require('@joplin/lib/path-utils');
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
@@ -218,6 +218,9 @@ describe('MdToHtml', () => {
|
||||
const mdToHtmlLinkifyOn = newTestMdToHtml({
|
||||
pluginOptions: {
|
||||
linkify: { enabled: true },
|
||||
link_open: {
|
||||
linkRenderingType: LinkRenderingType.HrefHandler,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -227,29 +230,52 @@ describe('MdToHtml', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const renderOptions = {
|
||||
bodyOnly: true,
|
||||
plainResourceRendering: true,
|
||||
linkRenderingType: LinkRenderingType.HrefHandler,
|
||||
};
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const [input, expectedLinkifyOff, expectedLinkifyOn] = testCase;
|
||||
|
||||
{
|
||||
const actual = await mdToHtmlLinkifyOn.render(input, null, {
|
||||
bodyOnly: true,
|
||||
plainResourceRendering: true,
|
||||
});
|
||||
const actual = await mdToHtmlLinkifyOn.render(input, null, renderOptions);
|
||||
|
||||
expect(actual.html).toBe(expectedLinkifyOn);
|
||||
}
|
||||
|
||||
{
|
||||
const actual = await mdToHtmlLinkifyOff.render(input, null, {
|
||||
bodyOnly: true,
|
||||
plainResourceRendering: true,
|
||||
});
|
||||
const actual = await mdToHtmlLinkifyOff.render(input, null, renderOptions);
|
||||
|
||||
expect(actual.html).toBe(expectedLinkifyOff);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
it.each([
|
||||
'[test](http://example.com/)',
|
||||
'[test](mailto:test@example.com)',
|
||||
])('should add onclick handlers to links when linkRenderingType is JavaScriptHandler (%j)', async (markdown) => {
|
||||
const mdToHtml = newTestMdToHtml();
|
||||
|
||||
const renderWithoutOnClickOptions = {
|
||||
bodyOnly: true,
|
||||
linkRenderingType: LinkRenderingType.HrefHandler,
|
||||
};
|
||||
expect(
|
||||
(await mdToHtml.render(markdown, undefined, renderWithoutOnClickOptions)).html,
|
||||
).not.toContain('onclick');
|
||||
|
||||
const renderWithOnClickOptions = {
|
||||
bodyOnly: true,
|
||||
linkRenderingType: LinkRenderingType.JavaScriptHandler,
|
||||
};
|
||||
expect(
|
||||
(await mdToHtml.render(markdown, undefined, renderWithOnClickOptions)).html,
|
||||
).toMatch(/<a data-from-md .*onclick=['"].*['"].*>/);
|
||||
});
|
||||
|
||||
it('should return attributes of line numbers', (async () => {
|
||||
const mdToHtml = newTestMdToHtml();
|
||||
|
||||
@@ -326,4 +352,12 @@ describe('MdToHtml', () => {
|
||||
expect(html).toContain('Inline</span>');
|
||||
expect(html).toContain('Block</span>');
|
||||
});
|
||||
|
||||
it('should sanitize KaTeX errors', async () => {
|
||||
const markdown = '$\\a<svg>$';
|
||||
const renderResult = await newTestMdToHtml().render(markdown, null, { bodyOnly: true });
|
||||
|
||||
// Should not contain the HTML in unsanitized form
|
||||
expect(renderResult.html).not.toContain('<svg>');
|
||||
});
|
||||
});
|
||||
|
1
packages/app-desktop/.gitignore
vendored
1
packages/app-desktop/.gitignore
vendored
@@ -24,3 +24,4 @@ build/defaultPlugins/
|
||||
build/7zip/7za
|
||||
build/7zip/7za.exe
|
||||
sentry.properties
|
||||
downloads/
|
||||
|
@@ -5,7 +5,7 @@ import type ShimType from '@joplin/lib/shim';
|
||||
const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
|
||||
import { BrowserWindow, Tray, screen } from 'electron';
|
||||
import { BrowserWindow, Tray, WebContents, screen } from 'electron';
|
||||
import bridge from './bridge';
|
||||
const url = require('url');
|
||||
const path = require('path');
|
||||
@@ -232,14 +232,35 @@ export default class ElectronAppWrapper {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
|
||||
this.win_.webContents.on('will-frame-navigate', event => {
|
||||
// If the link changes the URL of the browser window,
|
||||
if (event.isMainFrame) {
|
||||
event.preventDefault();
|
||||
void bridge().openExternal(event.url);
|
||||
}
|
||||
});
|
||||
const addWindowEventHandlers = (webContents: WebContents) => {
|
||||
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
|
||||
webContents.on('will-frame-navigate', event => {
|
||||
// If the link changes the URL of the browser window,
|
||||
if (event.isMainFrame) {
|
||||
event.preventDefault();
|
||||
void bridge().openExternal(event.url);
|
||||
}
|
||||
});
|
||||
|
||||
// Override calls to window.open and links with target="_blank": Open most in a browser instead
|
||||
// of Electron:
|
||||
webContents.setWindowOpenHandler((event) => {
|
||||
if (event.url === 'about:blank') {
|
||||
// Script-controlled pages: Used for opening notes in new windows
|
||||
return {
|
||||
action: 'allow',
|
||||
};
|
||||
} else if (event.url.match(/^https?:\/\//)) {
|
||||
void bridge().openExternal(event.url);
|
||||
}
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
webContents.on('did-create-window', (event) => {
|
||||
addWindowEventHandlers(event.webContents);
|
||||
});
|
||||
};
|
||||
addWindowEventHandlers(this.win_.webContents);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
this.win_.on('close', (event: any) => {
|
||||
@@ -332,6 +353,10 @@ export default class ElectronAppWrapper {
|
||||
this.updaterService_.updateApp();
|
||||
});
|
||||
|
||||
ipcMain.on('check-for-updates', () => {
|
||||
void this.updaterService_.checkForUpdates(true);
|
||||
});
|
||||
|
||||
// Let us register listeners on the window, so we can update the state
|
||||
// automatically (the listeners will be removed when the window is closed)
|
||||
// and restore the maximized or full screen state
|
||||
@@ -470,7 +495,7 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
|
||||
// Electron's autoUpdater has to be init from the main process
|
||||
public async initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
||||
public initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
if (!this.updaterService_) {
|
||||
this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases);
|
||||
@@ -482,7 +507,7 @@ export default class ElectronAppWrapper {
|
||||
private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => {
|
||||
this.stopPeriodicUpdateCheck();
|
||||
this.updatePollInterval_ = setInterval(() => {
|
||||
void this.updaterService_.checkForUpdates();
|
||||
void this.updaterService_.checkForUpdates(false);
|
||||
}, updateInterval);
|
||||
setTimeout(this.updaterService_.checkForUpdates, initialUpdateStartup);
|
||||
};
|
||||
@@ -491,6 +516,7 @@ export default class ElectronAppWrapper {
|
||||
if (this.updatePollInterval_) {
|
||||
clearInterval(this.updatePollInterval_);
|
||||
this.updatePollInterval_ = null;
|
||||
this.updaterService_ = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -7,7 +7,7 @@ const distPath = path.join(__dirname, distDirName);
|
||||
|
||||
const generateChecksumFile = () => {
|
||||
if (os.platform() !== 'linux') {
|
||||
return []; // SHA-512 is only for AppImage
|
||||
return; // SHA-512 is only for AppImage
|
||||
}
|
||||
|
||||
let appImageName = '';
|
||||
@@ -28,11 +28,13 @@ const generateChecksumFile = () => {
|
||||
const sha512FileName = `${appImageName}.sha512`;
|
||||
const sha512FilePath = path.join(distPath, sha512FileName);
|
||||
fs.writeFileSync(sha512FilePath, checksum);
|
||||
return [sha512FilePath];
|
||||
return sha512FilePath;
|
||||
};
|
||||
|
||||
const mainHook = () => {
|
||||
generateChecksumFile();
|
||||
const sha512FilePath = generateChecksumFile();
|
||||
const outputFiles = [sha512FilePath].filter(item => item);
|
||||
return outputFiles;
|
||||
};
|
||||
|
||||
exports.default = mainHook;
|
||||
|
@@ -404,9 +404,9 @@ class Application extends BaseApplication {
|
||||
eventManager.on(EventName.ResourceChange, handleResourceChange);
|
||||
}
|
||||
|
||||
private async setupAutoUpdaterService() {
|
||||
private setupAutoUpdaterService() {
|
||||
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||
await bridge().electronApp().initializeAutoUpdaterService(
|
||||
bridge().electronApp().initializeAutoUpdaterService(
|
||||
Logger.create('AutoUpdaterService'),
|
||||
Setting.value('env') === 'dev',
|
||||
Setting.value('autoUpdate.includePreReleases'),
|
||||
@@ -449,6 +449,8 @@ class Application extends BaseApplication {
|
||||
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
||||
await injectCustomStyles('appStyles', Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP));
|
||||
|
||||
this.setupAutoUpdaterService();
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||
AlarmService.setLogger(reg.logger());
|
||||
|
||||
@@ -698,8 +700,6 @@ class Application extends BaseApplication {
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
});
|
||||
|
||||
await this.setupAutoUpdaterService();
|
||||
|
||||
// setTimeout(() => {
|
||||
// void populateDatabase(reg.db(), {
|
||||
// clearDatabase: true,
|
||||
|
@@ -1,25 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const distDirName = 'dist';
|
||||
const distPath = path.join(__dirname, distDirName);
|
||||
|
||||
const renameLatestYmlFile = () => {
|
||||
if (os.platform() === 'darwin' && process.arch === 'arm64') {
|
||||
const latestMacFilePath = path.join(distPath, 'latest-mac.yml');
|
||||
const renamedMacFilePath = path.join(distPath, 'latest-mac-arm64.yml');
|
||||
|
||||
if (fs.existsSync(latestMacFilePath)) {
|
||||
fs.renameSync(latestMacFilePath, renamedMacFilePath);
|
||||
return [renamedMacFilePath];
|
||||
} else {
|
||||
throw new Error('latest-mac.yml not found!');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mainHook = () => {
|
||||
renameLatestYmlFile();
|
||||
};
|
||||
|
||||
exports.default = mainHook;
|
@@ -234,7 +234,7 @@ export default function(props: Props) {
|
||||
return (
|
||||
<CellFooter>
|
||||
<NeedUpgradeMessage>
|
||||
{PluginService.instance().describeIncompatibility(props.manifest)}
|
||||
{PluginService.instance().describeIncompatibility(item.manifest)}
|
||||
</NeedUpgradeMessage>
|
||||
</CellFooter>
|
||||
);
|
||||
|
@@ -116,7 +116,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td style={missingPasswordCellStyle}>
|
||||
<td style={passwordChecks[masterKeyId] ? theme.textStyle : missingPasswordCellStyle}>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={_('Enter password')}
|
||||
|
@@ -2,7 +2,6 @@
|
||||
import * as addProfile from './addProfile';
|
||||
import * as commandPalette from './commandPalette';
|
||||
import * as deleteFolder from './deleteFolder';
|
||||
import * as deleteNote from './deleteNote';
|
||||
import * as duplicateNote from './duplicateNote';
|
||||
import * as editAlarm from './editAlarm';
|
||||
import * as exportPdf from './exportPdf';
|
||||
@@ -20,7 +19,6 @@ import * as openItem from './openItem';
|
||||
import * as openNote from './openNote';
|
||||
import * as openPdfViewer from './openPdfViewer';
|
||||
import * as openTag from './openTag';
|
||||
import * as permanentlyDeleteNote from './permanentlyDeleteNote';
|
||||
import * as print from './print';
|
||||
import * as renameFolder from './renameFolder';
|
||||
import * as renameTag from './renameTag';
|
||||
@@ -52,7 +50,6 @@ const index: any[] = [
|
||||
addProfile,
|
||||
commandPalette,
|
||||
deleteFolder,
|
||||
deleteNote,
|
||||
duplicateNote,
|
||||
editAlarm,
|
||||
exportPdf,
|
||||
@@ -70,7 +67,6 @@ const index: any[] = [
|
||||
openNote,
|
||||
openPdfViewer,
|
||||
openTag,
|
||||
permanentlyDeleteNote,
|
||||
print,
|
||||
renameFolder,
|
||||
renameTag,
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const logger = Logger.create('commands/moveToFolder');
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'moveToFolder',
|
||||
@@ -11,19 +17,44 @@ export const declaration: CommandDeclaration = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteIds: string[] = null) => {
|
||||
noteIds = noteIds || context.state.selectedNoteIds;
|
||||
execute: async (context: CommandContext, itemIds: string[] = null) => {
|
||||
itemIds = itemIds || context.state.selectedNoteIds;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const folders: any[] = await Folder.sortFolderTree();
|
||||
let allAreFolders = true;
|
||||
const itemIdToType = new Map<string, ModelType>();
|
||||
for (const id of itemIds) {
|
||||
const item = await BaseItem.loadItemById(id);
|
||||
itemIdToType.set(id, item.type_);
|
||||
|
||||
if (item.type_ !== ModelType.Folder) {
|
||||
allAreFolders = false;
|
||||
}
|
||||
}
|
||||
|
||||
const folders = await Folder.sortFolderTree();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const startFolders: any[] = [];
|
||||
const maxDepth = 15;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const addOptions = (folders: any[], depth: number) => {
|
||||
// It's okay for folders (but not notes) to have no parent folder:
|
||||
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);
|
||||
}
|
||||
@@ -40,8 +71,25 @@ export const runtime = (comp: any): CommandRuntime => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onClose: async (answer: any) => {
|
||||
if (answer) {
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
await Note.moveToFolder(noteIds[i], answer.value);
|
||||
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 });
|
||||
|
@@ -26,6 +26,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
|
||||
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { EventName } from '@joplin/lib/eventManager';
|
||||
import { ipcRenderer } from 'electron';
|
||||
const packageInfo: PackageInfo = require('../packageInfo.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Menu = bridge().Menu;
|
||||
@@ -575,7 +576,12 @@ function useMenu(props: Props) {
|
||||
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled']));
|
||||
|
||||
function _checkForUpdates() {
|
||||
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||
ipcRenderer.send('check-for-updates');
|
||||
} else {
|
||||
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function _showAbout() {
|
||||
|
@@ -28,6 +28,7 @@ import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
|
||||
import Toolbar from '../Toolbar';
|
||||
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
@@ -318,6 +319,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
return output;
|
||||
}, [styles.cellViewer, props.visiblePanes]);
|
||||
|
||||
useRefocusOnVisiblePaneChange({ editorRef, webviewRef, visiblePanes: props.visiblePanes });
|
||||
|
||||
useEditorSearchHandler({
|
||||
setLocalSearchResultCount: props.setLocalSearchResultCount,
|
||||
searchMarkers: props.searchMarkers,
|
||||
|
@@ -88,7 +88,7 @@ const useEditorCommands = (props: Props) => {
|
||||
editorRef.current.updateBody(newBody);
|
||||
}
|
||||
},
|
||||
textHorizontalRule: () => editorRef.current.insertText('* * *'),
|
||||
textHorizontalRule: () => editorRef.current.execCommand(EditorCommandType.InsertHorizontalRule),
|
||||
'editor.execCommand': (value: CommandValue) => {
|
||||
if (!('args' in value)) value.args = [];
|
||||
|
||||
|
@@ -27,6 +27,10 @@ const useKeymap = (editorControl: CodeMirrorControl) => {
|
||||
binding.accelerator, CodeMirrorVersion.CodeMirror6,
|
||||
),
|
||||
run: () => {
|
||||
if (!CommandService.instance().isEnabled(binding.command)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void CommandService.instance().execute(binding.command);
|
||||
return true;
|
||||
},
|
||||
|
@@ -0,0 +1,44 @@
|
||||
import { RefObject, useRef, useEffect } from 'react';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import NoteTextViewer from '../../../../../NoteTextViewer';
|
||||
|
||||
interface Props {
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
webviewRef: RefObject<NoteTextViewer>;
|
||||
visiblePanes: string[];
|
||||
}
|
||||
|
||||
const useRefocusOnVisiblePaneChange = ({ editorRef, webviewRef, visiblePanes }: Props) => {
|
||||
const lastVisiblePanes = useRef(visiblePanes);
|
||||
useEffect(() => {
|
||||
const editorHasFocus = editorRef.current?.cm6?.dom?.contains(document.activeElement);
|
||||
const viewerHasFocus = webviewRef.current?.hasFocus();
|
||||
|
||||
const lastHadViewer = lastVisiblePanes.current.includes('viewer');
|
||||
const hasViewer = visiblePanes.includes('viewer');
|
||||
const lastHadEditor = lastVisiblePanes.current.includes('editor');
|
||||
const hasEditor = visiblePanes.includes('editor');
|
||||
|
||||
const viewerJustHidden = lastHadViewer && !hasViewer;
|
||||
if (viewerJustHidden && viewerHasFocus) {
|
||||
focus('CodeMirror/refocusEditor1', editorRef.current);
|
||||
}
|
||||
|
||||
// Jump focus to the editor just after showing it -- this assumes that the user
|
||||
// shows the editor to start editing the note.
|
||||
const editorJustShown = !lastHadEditor && hasEditor;
|
||||
if (editorJustShown && viewerHasFocus) {
|
||||
focus('CodeMirror/refocusEditor2', editorRef.current);
|
||||
}
|
||||
|
||||
const editorJustHidden = lastHadEditor && !hasEditor;
|
||||
if (editorJustHidden && editorHasFocus) {
|
||||
focus('CodeMirror/refocusViewer', webviewRef.current);
|
||||
}
|
||||
|
||||
lastVisiblePanes.current = visiblePanes;
|
||||
}, [visiblePanes, editorRef, webviewRef]);
|
||||
};
|
||||
|
||||
export default useRefocusOnVisiblePaneChange;
|
@@ -411,6 +411,20 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
color: ${theme.color};
|
||||
}
|
||||
|
||||
.tox .tox-dialog__body-nav-item {
|
||||
color: ${theme.color};
|
||||
}
|
||||
|
||||
.tox .tox-dialog__body-nav-item[aria-selected=true] {
|
||||
color: ${theme.color3};
|
||||
border-color: ${theme.color3};
|
||||
background-color: ${theme.backgroundColor3};
|
||||
}
|
||||
|
||||
.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg {
|
||||
fill: ${theme.color};
|
||||
}
|
||||
|
||||
.tox .tox-collection--list .tox-collection__item--active {
|
||||
color: ${theme.backgroundColor};
|
||||
}
|
||||
@@ -655,7 +669,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// Handle the first table row as table header.
|
||||
// https://www.tiny.cloud/docs/plugins/table/#table_header_type
|
||||
table_header_type: 'sectionCells',
|
||||
table_resize_bars: false,
|
||||
language_url: ['en_US', 'en_GB'].includes(language) ? undefined : `${bridge().vendorDir()}/lib/tinymce/langs/${language}`,
|
||||
toolbar: toolbar.join(' '),
|
||||
localization_function: _,
|
||||
|
@@ -218,15 +218,6 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
}
|
||||
}, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]);
|
||||
|
||||
useWindowCommandHandler({
|
||||
dispatch: props.dispatch,
|
||||
setShowLocalSearch,
|
||||
noteSearchBarRef,
|
||||
editorRef,
|
||||
titleInputRef,
|
||||
setFormNote,
|
||||
});
|
||||
|
||||
const onDrop = useDropHandler({ editorRef });
|
||||
|
||||
const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]);
|
||||
@@ -234,6 +225,15 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]);
|
||||
|
||||
useWindowCommandHandler({
|
||||
dispatch: props.dispatch,
|
||||
setShowLocalSearch,
|
||||
noteSearchBarRef,
|
||||
editorRef,
|
||||
titleInputRef,
|
||||
onBodyChange,
|
||||
});
|
||||
|
||||
// const onTitleKeydown = useCallback((event:any) => {
|
||||
// const keyCode = event.keyCode;
|
||||
|
||||
|
@@ -1,34 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { ChangeEvent, useCallback } from 'react';
|
||||
import { ChangeEvent, useCallback, useRef } from 'react';
|
||||
import NoteToolbar from '../../NoteToolbar/NoteToolbar';
|
||||
import { buildStyle } from '@joplin/lib/theme';
|
||||
import time from '@joplin/lib/time';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${props => props.theme.editorPaddingLeft}px;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
const InfoGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
border-top: 1px solid ${props => props.theme.dividerColor};
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -75,6 +51,33 @@ function styles_(props: Props) {
|
||||
});
|
||||
}
|
||||
|
||||
const useReselectHandlers = () => {
|
||||
const lastTitleFocus = useRef([0, 0]);
|
||||
const lastTitleValue = useRef('');
|
||||
|
||||
const onTitleBlur: React.FocusEventHandler<HTMLInputElement> = useCallback((event) => {
|
||||
const titleElement = event.currentTarget;
|
||||
lastTitleFocus.current = [titleElement.selectionStart, titleElement.selectionEnd];
|
||||
lastTitleValue.current = titleElement.value;
|
||||
}, []);
|
||||
|
||||
const onTitleFocus: React.FocusEventHandler<HTMLInputElement> = useCallback((event) => {
|
||||
const titleElement = event.currentTarget;
|
||||
// By default, focusing the note title bar can cause its content to become selected. We override
|
||||
// this with a more reasonable default:
|
||||
if (titleElement.selectionStart === 0 && titleElement.selectionEnd === titleElement.value.length) {
|
||||
if (lastTitleValue.current !== titleElement.value) {
|
||||
titleElement.selectionStart = titleElement.value.length;
|
||||
} else {
|
||||
titleElement.selectionStart = lastTitleFocus.current[0];
|
||||
titleElement.selectionEnd = lastTitleFocus.current[1];
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { onTitleBlur, onTitleFocus };
|
||||
};
|
||||
|
||||
export default function NoteTitleBar(props: Props) {
|
||||
const styles = styles_(props);
|
||||
|
||||
@@ -88,6 +91,8 @@ export default function NoteTitleBar(props: Props) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { onTitleFocus, onTitleBlur } = useReselectHandlers();
|
||||
|
||||
function renderTitleBarDate() {
|
||||
return <span className="updated-time-label" style={styles.titleDate}>{time.formatMsToLocal(props.noteUserUpdatedTime)}</span>;
|
||||
}
|
||||
@@ -101,7 +106,7 @@ export default function NoteTitleBar(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<div className='note-title-wrapper'>
|
||||
<input
|
||||
className="title-input"
|
||||
type="text"
|
||||
@@ -111,12 +116,14 @@ export default function NoteTitleBar(props: Props) {
|
||||
readOnly={props.disabled}
|
||||
onChange={props.onTitleChange}
|
||||
onKeyDown={onTitleKeydown}
|
||||
onFocus={onTitleFocus}
|
||||
onBlur={onTitleBlur}
|
||||
value={props.noteTitle}
|
||||
/>
|
||||
<InfoGroup>
|
||||
<div className='note-title-info-group'>
|
||||
{renderTitleBarDate()}
|
||||
{renderNoteToolbar()}
|
||||
</InfoGroup>
|
||||
</StyledRoot>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -38,7 +38,6 @@ const incompatiblePluginIds = [
|
||||
'ylc395.noteLinkSystem',
|
||||
'outline',
|
||||
'joplin.plugin.cmoptions',
|
||||
'plugin.calebjohn.MathMode',
|
||||
'com.ckant.joplin-plugin-better-code-blocks',
|
||||
// cSpell:enable
|
||||
];
|
||||
|
@@ -1,3 +1,5 @@
|
||||
|
||||
@use "./styles/warning-banner.scss";
|
||||
@use "./styles/warning-banner-link.scss";
|
||||
@use "./styles/note-title-info-group.scss";
|
||||
@use "./styles/note-title-wrapper.scss";
|
||||
|
@@ -0,0 +1,11 @@
|
||||
|
||||
.note-title-info-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
border-top: 1px solid var(--joplin-divider-color);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
|
||||
.note-title-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: var(--joplin-editor-padding-left);
|
||||
|
||||
@media (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
import { LinkRenderingType } from '@joplin/renderer/MdToHtml';
|
||||
import { MarkupToHtmlOptions } from './types';
|
||||
|
||||
export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
|
||||
@@ -7,7 +8,7 @@ export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
|
||||
checkboxRenderingType: 2,
|
||||
},
|
||||
link_open: {
|
||||
linkRenderingType: 2,
|
||||
linkRenderingType: LinkRenderingType.HrefHandler,
|
||||
},
|
||||
},
|
||||
replaceResourceInternalToExternalLinks: true,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
|
||||
import { NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
|
||||
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
|
||||
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import time from '@joplin/lib/time';
|
||||
@@ -12,7 +12,7 @@ const commandsWithDependencies = [
|
||||
require('../commands/pasteAsText'),
|
||||
];
|
||||
|
||||
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
|
||||
type OnBodyChange = (event: OnChangeEvent)=> void;
|
||||
|
||||
interface HookDependencies {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
@@ -23,13 +23,13 @@ interface HookDependencies {
|
||||
noteSearchBarRef: any;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
titleInputRef: RefObject<HTMLInputElement>;
|
||||
setFormNote: SetFormNoteCallback;
|
||||
onBodyChange: OnBodyChange;
|
||||
}
|
||||
|
||||
function editorCommandRuntime(
|
||||
declaration: CommandDeclaration,
|
||||
editorRef: RefObject<NoteBodyEditorRef>,
|
||||
setFormNote: SetFormNoteCallback,
|
||||
onBodyChange: OnBodyChange,
|
||||
): CommandRuntime {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -55,9 +55,7 @@ function editorCommandRuntime(
|
||||
value: args[0],
|
||||
});
|
||||
} else if (declaration.name === 'editor.setText') {
|
||||
setFormNote((prev: FormNote) => {
|
||||
return { ...prev, body: args[0] };
|
||||
});
|
||||
onBodyChange({ content: args[0], changeId: 0 });
|
||||
} else {
|
||||
return editorRef.current.execCommand({
|
||||
name: declaration.name,
|
||||
@@ -78,11 +76,11 @@ function editorCommandRuntime(
|
||||
}
|
||||
|
||||
export default function useWindowCommandHandler(dependencies: HookDependencies) {
|
||||
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, setFormNote } = dependencies;
|
||||
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange } = dependencies;
|
||||
|
||||
useEffect(() => {
|
||||
for (const declaration of editorCommandDeclarations) {
|
||||
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, setFormNote));
|
||||
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, onBodyChange));
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
@@ -105,5 +103,5 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
}
|
||||
};
|
||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, setFormNote]);
|
||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, onBodyChange]);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useRef, useEffect } from 'react';
|
||||
import { useMemo, useRef, useEffect, useCallback } from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { Props } from './utils/types';
|
||||
@@ -275,6 +275,12 @@ const NoteList = (props: Props) => {
|
||||
return output;
|
||||
}, [listRenderer.flow]);
|
||||
|
||||
const onContainerContextMenu = useCallback((event: React.MouseEvent) => {
|
||||
const isFromKeyboard = event.button === -1;
|
||||
if (event.isDefaultPrevented() || !isFromKeyboard) return;
|
||||
onItemContextMenu({ itemId: activeNoteId });
|
||||
}, [onItemContextMenu, activeNoteId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role='listbox'
|
||||
@@ -293,6 +299,7 @@ const NoteList = (props: Props) => {
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onDrop={onDrop}
|
||||
onContextMenu={onContainerContextMenu}
|
||||
>
|
||||
{renderEmptyList()}
|
||||
{renderFiller('top', topFillerStyle)}
|
||||
|
@@ -122,7 +122,7 @@ const useOnKeyDown = (
|
||||
}
|
||||
|
||||
if (noteIds.length) {
|
||||
if (key === 'Delete' && event.shiftKey) {
|
||||
if (key === 'Delete' && event.shiftKey || (key === 'Backspace' && event.metaKey && event.altKey)) {
|
||||
event.preventDefault();
|
||||
if (CommandService.instance().isEnabled('permanentlyDeleteNote')) {
|
||||
void CommandService.instance().execute('permanentlyDeleteNote', noteIds);
|
||||
@@ -153,14 +153,9 @@ const useOnKeyDown = (
|
||||
announceForAccessibility(!wasCompleted ? _('Complete') : _('Incomplete'));
|
||||
}
|
||||
|
||||
if (key === 'Tab') {
|
||||
if (key === 'Tab' && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.shiftKey) {
|
||||
void CommandService.instance().execute('focusElement', 'sideBar');
|
||||
} else {
|
||||
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
}
|
||||
void CommandService.instance().execute('focusElement', 'sideBar');
|
||||
}
|
||||
|
||||
if (key.toUpperCase() === 'A' && (event.ctrlKey || event.metaKey)) {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
@@ -6,6 +7,13 @@ import { Dispatch } from 'redux';
|
||||
import bridge from '../../../services/bridge';
|
||||
import NoteListUtils from '../../utils/NoteListUtils';
|
||||
|
||||
interface CustomContextMenuEvent {
|
||||
itemId: string;
|
||||
currentTarget?: undefined;
|
||||
preventDefault?: undefined;
|
||||
}
|
||||
type ContextMenuEvent = React.MouseEvent|CustomContextMenuEvent;
|
||||
|
||||
const useOnContextMenu = (
|
||||
selectedNoteIds: string[],
|
||||
selectedFolderId: string,
|
||||
@@ -15,10 +23,14 @@ const useOnContextMenu = (
|
||||
plugins: PluginStates,
|
||||
customCss: string,
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return useCallback((event: any) => {
|
||||
const currentNoteId = event.currentTarget.getAttribute('data-id');
|
||||
return useCallback((event: ContextMenuEvent) => {
|
||||
let currentNoteId = event.currentTarget?.getAttribute('data-id');
|
||||
if ('itemId' in event) {
|
||||
currentNoteId = event.itemId;
|
||||
}
|
||||
|
||||
if (!currentNoteId) return;
|
||||
event.preventDefault?.();
|
||||
|
||||
let noteIds = [];
|
||||
if (selectedNoteIds.indexOf(currentNoteId) < 0) {
|
||||
|
@@ -0,0 +1,55 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import useRootElement from './useRootElement';
|
||||
|
||||
describe('useRootElement', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
});
|
||||
|
||||
test('should find an element with a matching ID', async () => {
|
||||
const testElement = document.createElement('div');
|
||||
testElement.id = 'test-element-id';
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
const { result } = renderHook(useRootElement, {
|
||||
initialProps: testElement.id,
|
||||
});
|
||||
await act(async () => {
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
});
|
||||
expect(result.current).toBe(testElement);
|
||||
|
||||
testElement.remove();
|
||||
});
|
||||
|
||||
test('should redo the element search when the elementId prop changes', async () => {
|
||||
const testElement = document.createElement('div');
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
const { rerender, result } = renderHook(useRootElement, {
|
||||
initialProps: 'some-id-here',
|
||||
});
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
expect(result.current).toBe(null);
|
||||
|
||||
// Searching for another non-existent ID: Should not match
|
||||
rerender('updated-id');
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
expect(result.current).toBe(null);
|
||||
|
||||
// Should not match the first element if its ID is set to the original (search
|
||||
// should be cancelled).
|
||||
testElement.id = 'some-id-here';
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
expect(result.current).toBe(null);
|
||||
|
||||
// Should match if the element ID changes to the updated ID.
|
||||
await act(async () => {
|
||||
testElement.id = 'updated-id';
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
});
|
||||
expect(result.current).toBe(testElement);
|
||||
|
||||
testElement.remove();
|
||||
});
|
||||
});
|
@@ -6,7 +6,7 @@ const useRootElement = (elementId: string) => {
|
||||
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
const element = await waitForElement(document, elementId);
|
||||
const element = await waitForElement(document, elementId, event);
|
||||
if (event.cancelled) return;
|
||||
setRootElement(element);
|
||||
}, [document, elementId]);
|
||||
|
@@ -26,8 +26,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
|
||||
private initialized_ = false;
|
||||
private domReady_ = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private webviewRef_: any;
|
||||
private webviewRef_: React.RefObject<HTMLIFrameElement>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private webviewListeners_: any = null;
|
||||
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
|
||||
@@ -131,10 +130,21 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
|
||||
|
||||
public focus() {
|
||||
if (this.webviewRef_.current) {
|
||||
// Calling focus on webviewRef_ seems to be necessary when NoteTextViewer.focus
|
||||
// is called outside of a user event (e.g. in a setTimeout) or during automated
|
||||
// tests:
|
||||
focus('NoteTextViewer::focus', this.webviewRef_.current);
|
||||
|
||||
// Calling .focus on this.webviewRef.current isn't sufficient.
|
||||
// To allow arrow-key scrolling, focus must also be set within the iframe:
|
||||
this.send('focus');
|
||||
}
|
||||
}
|
||||
|
||||
public hasFocus() {
|
||||
return this.webviewRef_.current?.contains(document.activeElement);
|
||||
}
|
||||
|
||||
public tryInit() {
|
||||
if (!this.initialized_ && this.webviewRef_.current) {
|
||||
this.initWebview();
|
||||
|
@@ -151,6 +151,13 @@ const useOnRenderItem = (props: Props) => {
|
||||
}
|
||||
|
||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
||||
menu.append(new MenuItem({
|
||||
...menuUtils.commandToStatefulMenuItem('moveToFolder', [itemId]),
|
||||
// By default, enabled is based on the selected folder. However, the right-click
|
||||
// menu can be shown for unselected folders.
|
||||
enabled: true,
|
||||
}));
|
||||
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('openFolderDialog', { folderId: itemId })));
|
||||
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { StyledIconSpan, StyledIconI } from './styles';
|
||||
|
||||
interface Props {
|
||||
readonly themeId: number;
|
||||
@@ -36,8 +35,12 @@ export default function ToolbarButton(props: Props) {
|
||||
let icon = null;
|
||||
const iconName = getProp(props, 'iconName');
|
||||
if (iconName) {
|
||||
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
|
||||
icon = <IconClass className={iconName} aria-label='' hasTitle={!!title} role='img'/>;
|
||||
const iconProps: React.HTMLProps<HTMLDivElement> = {
|
||||
'aria-label': '',
|
||||
role: 'img',
|
||||
className: `toolbar-icon ${title ? '-has-title' : ''} ${iconName}`,
|
||||
};
|
||||
icon = isFontAwesomeIcon(iconName) ? <i {...iconProps} /> : <span {...iconProps} />;
|
||||
}
|
||||
|
||||
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
|
||||
|
@@ -1,19 +0,0 @@
|
||||
import { ThemeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const styled = require('styled-components').default;
|
||||
const { css } = require('styled-components');
|
||||
|
||||
interface IconProps {
|
||||
readonly theme: ThemeStyle;
|
||||
readonly hasTitle: boolean;
|
||||
}
|
||||
|
||||
const iconStyle = css<IconProps>`
|
||||
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
|
||||
color: ${(props: IconProps) => props.theme.color3};
|
||||
margin-right: ${(props: IconProps) => props.hasTitle ? 5 : 0}px;
|
||||
pointer-events: none; /* Need this to get button tooltip to work */
|
||||
`;
|
||||
|
||||
export const StyledIconI = styled.i`${iconStyle}`;
|
||||
export const StyledIconSpan = styled.span`${iconStyle}`;
|
@@ -1,4 +1,4 @@
|
||||
import { useContext, useCallback, useMemo } from 'react';
|
||||
import { useContext, useCallback, useMemo, useRef } from 'react';
|
||||
import { StateLastDeletion } from '@joplin/lib/reducer';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import NotyfContext from '../NotyfContext';
|
||||
@@ -9,6 +9,7 @@ import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Dispatch } from 'redux';
|
||||
import { NotyfNotification } from 'notyf';
|
||||
|
||||
interface Props {
|
||||
lastDeletion: StateLastDeletion;
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
|
||||
export default (props: Props) => {
|
||||
const notyfContext = useContext(NotyfContext);
|
||||
const notificationRef = useRef<NotyfNotification | null>(null);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
return themeStyle(props.themeId);
|
||||
@@ -39,7 +41,8 @@ export default (props: Props) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const onCancelClick = useCallback(async (event: any) => {
|
||||
notyf.dismissAll();
|
||||
notyf.dismiss(notificationRef.current);
|
||||
notificationRef.current = null;
|
||||
|
||||
const lastDeletion: StateLastDeletion = JSON.parse(event.currentTarget.getAttribute('data-lastDeletion'));
|
||||
|
||||
@@ -70,7 +73,8 @@ export default (props: Props) => {
|
||||
const linkId = `deletion-notification-cancel-${Math.floor(Math.random() * 1000000)}`;
|
||||
const cancelLabel = _('Cancel');
|
||||
|
||||
notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
|
||||
const notification = notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
|
||||
notificationRef.current = notification;
|
||||
|
||||
const element: HTMLAnchorElement = await waitForElement(document, linkId);
|
||||
if (event.cancelled) return;
|
||||
|
@@ -5,7 +5,7 @@ import NotyfContext from '../NotyfContext';
|
||||
import { UpdateInfo } from 'electron-updater';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
|
||||
import { NotyfNotification } from 'notyf';
|
||||
import { NotyfEvent, NotyfNotification } from 'notyf';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { htmlentities } from '@joplin/utils/html';
|
||||
import shim from '@joplin/lib/shim';
|
||||
@@ -16,6 +16,7 @@ interface UpdateNotificationProps {
|
||||
|
||||
export enum UpdateNotificationEvents {
|
||||
ApplyUpdate = 'apply-update',
|
||||
UpdateNotAvailable = 'update-not-available',
|
||||
Dismiss = 'dismiss-update-notification',
|
||||
}
|
||||
|
||||
@@ -86,17 +87,46 @@ const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
|
||||
notificationRef.current = notification;
|
||||
}, [notyf, theme]);
|
||||
|
||||
const handleUpdateNotAvailable = useCallback(() => {
|
||||
if (notificationRef.current) return;
|
||||
|
||||
const noUpdateMessageHtml = htmlentities(_('No updates available'));
|
||||
|
||||
const messageHtml = `
|
||||
<div class="update-notification" style="color: ${theme.color2};">
|
||||
${noUpdateMessageHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const notification: NotyfNotification = notyf.open({
|
||||
type: 'success',
|
||||
message: messageHtml,
|
||||
position: {
|
||||
x: 'right',
|
||||
y: 'bottom',
|
||||
},
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
notification.on(NotyfEvent.Dismiss, () => {
|
||||
notificationRef.current = null;
|
||||
});
|
||||
|
||||
notificationRef.current = notification;
|
||||
}, [notyf, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
ipcRenderer.on(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
|
||||
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||
|
||||
return () => {
|
||||
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
|
||||
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||
document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||
};
|
||||
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]);
|
||||
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleUpdateNotAvailable]);
|
||||
|
||||
|
||||
return (
|
||||
|
@@ -369,7 +369,7 @@
|
||||
ipc.focus = (event) => {
|
||||
const dummyID = 'joplin-content-focus-dummy';
|
||||
if (! document.getElementById(dummyID)) {
|
||||
const focusDummy = '<div style="width: 0; height: 0; overflow: hidden"><a id="' + dummyID + '" href="#">focus dummy</a></div>';
|
||||
const focusDummy = '<div style="width: 0; height: 0; overflow: hidden"><a id="' + dummyID + '" href="#">Note viewer top</a></div>';
|
||||
contentElement.insertAdjacentHTML("afterbegin", focusDummy);
|
||||
}
|
||||
const scrollTop = contentElement.scrollTop;
|
||||
@@ -733,6 +733,13 @@
|
||||
}));
|
||||
|
||||
document.addEventListener('click', webviewLib.logEnabledEventHandler(e => {
|
||||
// Links should all have custom click handlers. Allowing Electron to load custom links
|
||||
// can cause security issues, particularly if these links have the same domain as the
|
||||
// top-level page.
|
||||
if (e.target.hasAttribute('href')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.media-pdf').forEach(element => {
|
||||
if(!!element.contentWindow){
|
||||
element.contentWindow.postMessage({
|
||||
|
4
packages/app-desktop/gui/styles/dialog-anchor-node.scss
Normal file
4
packages/app-desktop/gui/styles/dialog-anchor-node.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
.dialog-anchor-node {
|
||||
display: none;
|
||||
}
|
@@ -5,4 +5,7 @@
|
||||
@use './flat-button.scss';
|
||||
@use './help-text.scss';
|
||||
@use './toolbar-button.scss';
|
||||
@use './toolbar-icon.scss';
|
||||
@use './editor-toolbar.scss';
|
||||
@use './user-webview-dialog-container.scss';
|
||||
@use './dialog-anchor-node.scss';
|
||||
|
11
packages/app-desktop/gui/styles/toolbar-icon.scss
Normal file
11
packages/app-desktop/gui/styles/toolbar-icon.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
.toolbar-icon {
|
||||
font-size: var(--joplin-toolbar-icon-size);
|
||||
color: var(--joplin-color3);
|
||||
margin-right: 0px;
|
||||
pointer-events: none; /* Need this to get button tooltip to work */
|
||||
|
||||
&.-has-title {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
|
||||
.user-webview-dialog-container {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
box-sizing: border-box;
|
||||
}
|
@@ -32,7 +32,7 @@ export default class NoteListUtils {
|
||||
|
||||
const menuUtils = new MenuUtils(cmdService);
|
||||
|
||||
const notes: NoteEntity[] = noteIds.map(id => BaseModel.byId(props.notes, id));
|
||||
const notes: NoteEntity[] = BaseModel.modelsByIds(props.notes, noteIds);
|
||||
|
||||
const singleNoteId = noteIds.length === 1 ? noteIds[0] : null;
|
||||
|
||||
|
@@ -36,7 +36,7 @@ test.describe('main', () => {
|
||||
await mainWindow.keyboard.type('New note content!');
|
||||
|
||||
// Should render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
const viewerFrame = editor.getNoteViewerFrameLocator();
|
||||
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ test.describe('main', () => {
|
||||
}
|
||||
|
||||
// Should render mermaid
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
const viewerFrame = editor.getNoteViewerFrameLocator();
|
||||
await expect(
|
||||
viewerFrame.locator('pre.mermaid text', { hasText: testCommitId }),
|
||||
).toBeVisible();
|
||||
@@ -115,7 +115,7 @@ test.describe('main', () => {
|
||||
await setMessageBoxResponse(electronApp, /^No/i);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
const viewerFrame = editor.getNoteViewerFrameLocator();
|
||||
const renderedImage = viewerFrame.getByAltText(filename);
|
||||
|
||||
const fullSize = await getImageSourceSize(renderedImage);
|
||||
@@ -136,50 +136,55 @@ test.describe('main', () => {
|
||||
expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]);
|
||||
});
|
||||
|
||||
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
for (const target of ['', '_blank']) {
|
||||
test(`clicking on an external link with target=${JSON.stringify(target)} should try to launch a browser`, async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Mock openExternal
|
||||
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
|
||||
return new Promise<string>(resolve => {
|
||||
const openExternal = async (url: string) => {
|
||||
resolve(url);
|
||||
};
|
||||
shell.openExternal = openExternal;
|
||||
// Mock openExternal
|
||||
const nextExternalUrlPromise = electronApp.evaluate(({ shell }) => {
|
||||
return new Promise<string>(resolve => {
|
||||
const openExternal = async (url: string) => {
|
||||
resolve(url);
|
||||
};
|
||||
shell.openExternal = openExternal;
|
||||
});
|
||||
});
|
||||
|
||||
// Create a test link
|
||||
const testLinkTitle = 'This is a test link!';
|
||||
const linkHref = 'https://joplinapp.org/';
|
||||
|
||||
await mainWindow.evaluate(({ testLinkTitle, linkHref, target }) => {
|
||||
const testLink = document.createElement('a');
|
||||
testLink.textContent = testLinkTitle;
|
||||
testLink.onclick = () => {
|
||||
// We need to navigate by setting location.href -- clicking on a link
|
||||
// directly within the main window (i.e. not in a PDF viewer) doesn't
|
||||
// navigate.
|
||||
location.href = linkHref;
|
||||
};
|
||||
testLink.href = '#';
|
||||
|
||||
// Display on top of everything
|
||||
testLink.style.zIndex = '99999';
|
||||
testLink.style.position = 'fixed';
|
||||
testLink.style.top = '0';
|
||||
testLink.style.left = '0';
|
||||
if (target) {
|
||||
testLink.target = target;
|
||||
}
|
||||
|
||||
document.body.appendChild(testLink);
|
||||
}, { testLinkTitle, linkHref, target });
|
||||
|
||||
const testLink = mainWindow.getByText(testLinkTitle);
|
||||
await expect(testLink).toBeVisible();
|
||||
await testLink.click({ noWaitAfter: true });
|
||||
|
||||
expect(await nextExternalUrlPromise).toBe(linkHref);
|
||||
});
|
||||
|
||||
// Create a test link
|
||||
const testLinkTitle = 'This is a test link!';
|
||||
const linkHref = 'https://joplinapp.org/';
|
||||
|
||||
await mainWindow.evaluate(({ testLinkTitle, linkHref }) => {
|
||||
const testLink = document.createElement('a');
|
||||
testLink.textContent = testLinkTitle;
|
||||
testLink.onclick = () => {
|
||||
// We need to navigate by setting location.href -- clicking on a link
|
||||
// directly within the main window (i.e. not in a PDF viewer) doesn't
|
||||
// navigate.
|
||||
location.href = linkHref;
|
||||
};
|
||||
testLink.href = '#';
|
||||
|
||||
// Display on top of everything
|
||||
testLink.style.zIndex = '99999';
|
||||
testLink.style.position = 'fixed';
|
||||
testLink.style.top = '0';
|
||||
testLink.style.left = '0';
|
||||
|
||||
document.body.appendChild(testLink);
|
||||
}, { testLinkTitle, linkHref });
|
||||
|
||||
const testLink = mainWindow.getByText(testLinkTitle);
|
||||
await expect(testLink).toBeVisible();
|
||||
await testLink.click({ noWaitAfter: true });
|
||||
|
||||
expect(await nextExternalUrlPromise).toBe(linkHref);
|
||||
});
|
||||
}
|
||||
|
||||
test('should start in safe mode if profile-dir/force-safe-mode-on-next-start exists', async ({ profileDirectory }) => {
|
||||
await writeFile(join(profileDirectory, 'force-safe-mode-on-next-start'), 'true', 'utf8');
|
||||
|
@@ -3,6 +3,7 @@ import MainScreen from './models/MainScreen';
|
||||
import { join } from 'path';
|
||||
import getImageSourceSize from './util/getImageSourceSize';
|
||||
import setFilePickerResponse from './util/setFilePickerResponse';
|
||||
import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
|
||||
|
||||
test.describe('markdownEditor', () => {
|
||||
@@ -13,13 +14,18 @@ test.describe('markdownEditor', () => {
|
||||
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
|
||||
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
|
||||
await importedFolder.waitFor();
|
||||
await importedFolder.click();
|
||||
|
||||
await mainScreen.noteList.focusContent(electronApp);
|
||||
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
|
||||
await importedHtmlFileItem.click();
|
||||
// Retry -- focusing the imported-folder may fail in some cases
|
||||
await expect(async () => {
|
||||
await importedFolder.click();
|
||||
|
||||
const viewerFrame = mainScreen.noteEditor.getNoteViewerIframe();
|
||||
await mainScreen.noteList.focusContent(electronApp);
|
||||
|
||||
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
|
||||
await importedHtmlFileItem.click({ timeout: 300 });
|
||||
}).toPass();
|
||||
|
||||
const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator();
|
||||
// Should render headers
|
||||
await expect(viewerFrame.locator('h1')).toHaveText('Test HTML file!');
|
||||
|
||||
@@ -39,7 +45,7 @@ test.describe('markdownEditor', () => {
|
||||
await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'small-pdf.pdf')]);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
const viewerFrame = mainScreen.noteEditor.getNoteViewerIframe();
|
||||
const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator();
|
||||
const pdfLink = viewerFrame.getByText('small-pdf.pdf');
|
||||
await expect(pdfLink).toBeVisible();
|
||||
|
||||
@@ -72,6 +78,24 @@ test.describe('markdownEditor', () => {
|
||||
await mainScreen.noteEditor.toggleEditorsButton.click();
|
||||
|
||||
await expectToBeRendered();
|
||||
|
||||
// Clicking on the PDF link should attempt to open it in a viewer
|
||||
await expect(pdfLink).toBeVisible();
|
||||
|
||||
const nextOpenFilePromise = electronApp.evaluate(({ shell }) => {
|
||||
return new Promise<string>(resolve => {
|
||||
const openPath = async (url: string) => {
|
||||
resolve(url);
|
||||
return '';
|
||||
};
|
||||
shell.openPath = openPath;
|
||||
});
|
||||
});
|
||||
await pdfLink.click();
|
||||
expect(await nextOpenFilePromise).toMatch(/\.pdf$/);
|
||||
|
||||
// Should not have rendered something else in the viewer frame
|
||||
await expectToBeRendered();
|
||||
});
|
||||
|
||||
test('preview pane should render video attachments', async ({ mainWindow, electronApp }) => {
|
||||
@@ -83,7 +107,7 @@ test.describe('markdownEditor', () => {
|
||||
await setFilePickerResponse(electronApp, [join(__dirname, 'resources', 'video.mp4')]);
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
const videoLocator = editor.getNoteViewerIframe().locator('video');
|
||||
const videoLocator = editor.getNoteViewerFrameLocator().locator('video');
|
||||
const expectVideoToRender = async () => {
|
||||
await expect(videoLocator).toBeSeekableMediaElement(6.9, 7);
|
||||
};
|
||||
@@ -149,7 +173,7 @@ test.describe('markdownEditor', () => {
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('This is a test of search. `Test inline code`');
|
||||
|
||||
const viewer = noteEditor.getNoteViewerIframe();
|
||||
const viewer = noteEditor.getNoteViewerFrameLocator();
|
||||
await expect(viewer.locator('h1')).toHaveText('Testing');
|
||||
|
||||
const matches = viewer.locator('mark');
|
||||
@@ -190,5 +214,39 @@ test.describe('markdownEditor', () => {
|
||||
await expect(noteEditor.codeMirrorEditor).toBeVisible();
|
||||
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should move focus when the visible editor panes change', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
|
||||
await mainScreen.createNewNote('Note');
|
||||
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.type('test');
|
||||
const focusInMarkdownEditor = noteEditor.codeMirrorEditor.locator(':focus');
|
||||
await expect(focusInMarkdownEditor).toBeAttached();
|
||||
|
||||
const toggleEditorLayout = () => activateMainMenuItem(electronApp, 'Toggle editor layout');
|
||||
|
||||
// Editor only
|
||||
await toggleEditorLayout();
|
||||
await expect(noteEditor.noteViewerContainer).not.toBeVisible();
|
||||
// Markdown editor should be focused
|
||||
await expect(focusInMarkdownEditor).toBeAttached();
|
||||
|
||||
// Viewer only
|
||||
await toggleEditorLayout();
|
||||
await expect(noteEditor.codeMirrorEditor).not.toBeVisible();
|
||||
// Viewer should be focused
|
||||
await expect(noteEditor.noteViewerContainer).toBeFocused();
|
||||
|
||||
// Viewer and editor
|
||||
await toggleEditorLayout();
|
||||
await expect(noteEditor.noteViewerContainer).toBeAttached();
|
||||
await expect(noteEditor.codeMirrorEditor).toBeVisible();
|
||||
// Editor should be focused
|
||||
await expect(focusInMarkdownEditor).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -14,10 +14,7 @@ export default class GoToAnything {
|
||||
|
||||
public async open(electronApp: ElectronApplication) {
|
||||
await this.mainScreen.waitFor();
|
||||
|
||||
if (!await activateMainMenuItem(electronApp, 'Goto Anything...')) {
|
||||
throw new Error('Menu item for opening Goto Anything not found');
|
||||
}
|
||||
await activateMainMenuItem(electronApp, 'Goto Anything...');
|
||||
|
||||
return this.waitFor();
|
||||
}
|
||||
@@ -33,4 +30,16 @@ export default class GoToAnything {
|
||||
public async expectToBeOpen() {
|
||||
await expect(this.containerLocator).toBeAttached();
|
||||
}
|
||||
|
||||
public async runCommand(electronApp: ElectronApplication, command: string) {
|
||||
if (!command.startsWith(':')) {
|
||||
command = `:${command}`;
|
||||
}
|
||||
|
||||
await this.open(electronApp);
|
||||
await this.inputLocator.fill(command);
|
||||
await this.containerLocator.locator('.match-highlight').first().waitFor();
|
||||
await this.inputLocator.press('Enter');
|
||||
await this.expectToBeClosed();
|
||||
}
|
||||
}
|
||||
|
@@ -46,12 +46,7 @@ export default class MainScreen {
|
||||
|
||||
public async openSettings(electronApp: ElectronApplication) {
|
||||
// Check both labels so this works on MacOS
|
||||
const openedWithPreferences = await activateMainMenuItem(electronApp, 'Preferences...');
|
||||
const openedWithOptions = await activateMainMenuItem(electronApp, 'Options');
|
||||
|
||||
if (!openedWithOptions && !openedWithPreferences) {
|
||||
throw new Error('Unable to find settings menu item in application menus.');
|
||||
}
|
||||
await activateMainMenuItem(electronApp, /^(Preferences\.\.\.|Options)$/);
|
||||
}
|
||||
|
||||
public async search(text: string) {
|
||||
@@ -61,10 +56,6 @@ export default class MainScreen {
|
||||
|
||||
public async importHtmlDirectory(electronApp: ElectronApplication, path: string) {
|
||||
await setFilePickerResponse(electronApp, [path]);
|
||||
const startedImport = await activateMainMenuItem(electronApp, 'HTML - HTML document (Directory)', 'Import');
|
||||
|
||||
if (!startedImport) {
|
||||
throw new Error('Unable to find HTML directory import menu item.');
|
||||
}
|
||||
await activateMainMenuItem(electronApp, 'HTML - HTML document (Directory)', 'Import');
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
import { expect } from '../util/test';
|
||||
|
||||
export default class NoteEditorPage {
|
||||
public readonly codeMirrorEditor: Locator;
|
||||
public readonly noteViewerContainer: Locator;
|
||||
public readonly richTextEditor: Locator;
|
||||
public readonly noteTitleInput: Locator;
|
||||
public readonly attachFileButton: Locator;
|
||||
@@ -12,7 +14,7 @@ export default class NoteEditorPage {
|
||||
public readonly viewerSearchInput: Locator;
|
||||
private readonly containerLocator: Locator;
|
||||
|
||||
public constructor(private readonly page: Page) {
|
||||
public constructor(page: Page) {
|
||||
this.containerLocator = page.locator('.rli-editor');
|
||||
this.codeMirrorEditor = this.containerLocator.locator('.cm-editor');
|
||||
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
||||
@@ -20,6 +22,7 @@ export default class NoteEditorPage {
|
||||
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
|
||||
this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors' });
|
||||
this.toggleEditorLayoutButton = this.containerLocator.getByRole('button', { name: 'Toggle editor layout' });
|
||||
this.noteViewerContainer = this.containerLocator.locator('iframe[src$="note-viewer/index.html"]');
|
||||
// The editor and viewer have slightly different search UI
|
||||
this.editorSearchInput = this.containerLocator.getByPlaceholder('Find');
|
||||
this.viewerSearchInput = this.containerLocator.getByPlaceholder('Search...');
|
||||
@@ -29,14 +32,39 @@ export default class NoteEditorPage {
|
||||
return this.containerLocator.getByRole('button', { name: title });
|
||||
}
|
||||
|
||||
public getNoteViewerIframe() {
|
||||
public async contentLocator() {
|
||||
const richTextBody = this.getRichTextFrameLocator().locator('body');
|
||||
const markdownEditor = this.codeMirrorEditor;
|
||||
|
||||
// Work around an issue where .or doesn't work with frameLocators.
|
||||
// See https://github.com/microsoft/playwright/issues/27688#issuecomment-1771403495
|
||||
await Promise.race([
|
||||
richTextBody.waitFor({ state: 'visible' }).catch(()=>{}),
|
||||
markdownEditor.waitFor({ state: 'visible' }).catch(()=>{}),
|
||||
]);
|
||||
if (await richTextBody.isVisible()) {
|
||||
return richTextBody;
|
||||
} else {
|
||||
return markdownEditor;
|
||||
}
|
||||
}
|
||||
|
||||
public async expectToHaveText(content: string) {
|
||||
// expect(...).toHaveText can fail in the Rich Text Editor (perhaps due to frame locators).
|
||||
// Using expect.poll refreshes the locator on each attempt, which seems to prevent flakiness.
|
||||
await expect.poll(
|
||||
async () => (await this.contentLocator()).textContent(),
|
||||
).toBe(content);
|
||||
}
|
||||
|
||||
public getNoteViewerFrameLocator() {
|
||||
// The note viewer can change content when the note re-renders. As such,
|
||||
// a new locator needs to be created after re-renders (and this can't be a
|
||||
// static property).
|
||||
return this.page.frameLocator('[src$="note-viewer/index.html"]');
|
||||
return this.noteViewerContainer.frameLocator(':scope');
|
||||
}
|
||||
|
||||
public getTinyMCEFrameLocator() {
|
||||
public getRichTextFrameLocator() {
|
||||
// We use frameLocator(':scope') to convert the richTextEditor Locator into
|
||||
// a FrameLocator. (:scope selects the locator itself).
|
||||
// https://playwright.dev/docs/api/class-framelocator
|
||||
@@ -51,4 +79,10 @@ export default class NoteEditorPage {
|
||||
await this.noteTitleInput.waitFor();
|
||||
await this.toggleEditorsButton.waitFor();
|
||||
}
|
||||
|
||||
public async goBack() {
|
||||
const backButton = this.toolbarButtonLocator('Back');
|
||||
await expect(backButton).not.toBeDisabled();
|
||||
await backButton.click();
|
||||
}
|
||||
}
|
||||
|
@@ -3,9 +3,11 @@ import { ElectronApplication, Locator, Page, expect } from '@playwright/test';
|
||||
|
||||
export default class NoteList {
|
||||
public readonly container: Locator;
|
||||
public readonly sortOrderButton: Locator;
|
||||
|
||||
public constructor(page: Page) {
|
||||
this.container = page.locator('.rli-noteList');
|
||||
this.sortOrderButton = this.container.getByRole('button', { name: 'Toggle sort order' });
|
||||
}
|
||||
|
||||
public waitFor() {
|
||||
@@ -13,14 +15,12 @@ export default class NoteList {
|
||||
}
|
||||
|
||||
private async sortBy(electronApp: ElectronApplication, sortMethod: string) {
|
||||
const success = await activateMainMenuItem(electronApp, sortMethod, 'Sort notes by');
|
||||
if (!success) {
|
||||
throw new Error(`Unable to find sorting menu item: ${sortMethod}`);
|
||||
}
|
||||
await activateMainMenuItem(electronApp, sortMethod, 'Sort notes by');
|
||||
}
|
||||
|
||||
public async sortByTitle(electronApp: ElectronApplication) {
|
||||
return this.sortBy(electronApp, 'Title');
|
||||
await this.sortBy(electronApp, 'Title');
|
||||
await expect(this.sortOrderButton).toHaveAttribute('title', /Toggle sort order field:[\n ]*title ->/);
|
||||
}
|
||||
|
||||
public async focusContent(electronApp: ElectronApplication) {
|
||||
|
@@ -23,10 +23,7 @@ export default class Sidebar {
|
||||
}
|
||||
|
||||
private async sortBy(electronApp: ElectronApplication, option: string) {
|
||||
const success = await activateMainMenuItem(electronApp, option, 'Sort notebooks by');
|
||||
if (!success) {
|
||||
throw new Error(`Failed to find menu item: ${option}`);
|
||||
}
|
||||
await activateMainMenuItem(electronApp, option, 'Sort notebooks by');
|
||||
}
|
||||
|
||||
public async sortByDate(electronApp: ElectronApplication) {
|
||||
|
@@ -32,7 +32,7 @@ test.describe('noteList', () => {
|
||||
await mainWindow.keyboard.type('[Testing...](http://example.com/)');
|
||||
|
||||
// Wait to render
|
||||
await expect(editor.getNoteViewerIframe().locator('a', { hasText: 'Testing...' })).toBeVisible();
|
||||
await expect(editor.getNoteViewerFrameLocator().locator('a', { hasText: 'Testing...' })).toBeVisible();
|
||||
|
||||
// Updating the title should force the sidebar to update sooner
|
||||
await expect(editor.noteTitleInput).toHaveValue('note-1');
|
||||
@@ -91,7 +91,14 @@ test.describe('noteList', () => {
|
||||
await noteList.focusContent(electronApp);
|
||||
// The most recently-created note should be visible
|
||||
const note4Item = noteList.getNoteItemByTitle('note_4');
|
||||
const note3Item = noteList.getNoteItemByTitle('note_3');
|
||||
const note2Item = noteList.getNoteItemByTitle('note_2');
|
||||
const note1Item = noteList.getNoteItemByTitle('note_1');
|
||||
await expect(note4Item).toBeVisible();
|
||||
await expect(note3Item).toBeVisible();
|
||||
await expect(note2Item).toBeVisible();
|
||||
await expect(note1Item).toBeVisible();
|
||||
|
||||
await noteList.expectNoteToBeSelected('note_4');
|
||||
|
||||
await noteList.container.press('ArrowUp');
|
||||
|
32
packages/app-desktop/integration-tests/pluginApi.spec.ts
Normal file
32
packages/app-desktop/integration-tests/pluginApi.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { test } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
|
||||
test.describe('pluginApi', () => {
|
||||
for (const richTextEditor of [false, true]) {
|
||||
test(`the editor.setText command should update the current note (use RTE: ${richTextEditor})`, async ({ startAppWithPlugins }) => {
|
||||
const { app, mainWindow } = await startAppWithPlugins(['resources/test-plugins/execCommand.js']);
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.createNewNote('First note');
|
||||
const editor = mainScreen.noteEditor;
|
||||
|
||||
await editor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.type('This content should be overwritten.');
|
||||
|
||||
if (richTextEditor) {
|
||||
await editor.toggleEditorsButton.click();
|
||||
await editor.richTextEditor.click();
|
||||
}
|
||||
|
||||
await mainScreen.goToAnything.runCommand(app, 'testUpdateEditorText');
|
||||
await editor.expectToHaveText('PASS');
|
||||
|
||||
// Should still have the same text after switching notes:
|
||||
await mainScreen.createNewNote('Second note');
|
||||
await editor.goBack();
|
||||
|
||||
await editor.expectToHaveText('PASS');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -0,0 +1,31 @@
|
||||
// Allows referencing the Joplin global:
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
// Allows the `joplin-manifest` block comment:
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "org.joplinapp.plugins.example.execCommand",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "3.1",
|
||||
"name": "JS Bundle test",
|
||||
"description": "JS Bundle Test plugin",
|
||||
"version": "1.0.0",
|
||||
"author": "",
|
||||
"homepage_url": "https://joplinapp.org"
|
||||
}
|
||||
*/
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.commands.register({
|
||||
name: 'testUpdateEditorText',
|
||||
label: 'Test setting the editor\'s text with editor.setText',
|
||||
iconName: 'fas fa-drum',
|
||||
execute: async () => {
|
||||
await joplin.commands.execute('editor.setText', 'PASS');
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
@@ -18,7 +18,7 @@ test.describe('richTextEditor', () => {
|
||||
await editor.attachFileButton.click();
|
||||
|
||||
// Wait to render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
const viewerFrame = editor.getNoteViewerFrameLocator();
|
||||
await viewerFrame.locator('a[data-from-md]').waitFor();
|
||||
|
||||
// Should have an attached resource
|
||||
@@ -38,7 +38,7 @@ test.describe('richTextEditor', () => {
|
||||
await editor.richTextEditor.waitFor();
|
||||
|
||||
// Edit the note to cause the original content to update
|
||||
await editor.getTinyMCEFrameLocator().locator('a').click();
|
||||
await editor.getRichTextFrameLocator().locator('a').click();
|
||||
await mainWindow.keyboard.type('Test...');
|
||||
|
||||
await editor.toggleEditorsButton.click();
|
||||
@@ -70,7 +70,7 @@ test.describe('richTextEditor', () => {
|
||||
|
||||
// Click on the attached file URL
|
||||
const openPathResult = waitForNextOpenPath(electronApp);
|
||||
const targetLink = editor.getTinyMCEFrameLocator().getByRole('link', { name: basename(pathToAttach) });
|
||||
const targetLink = editor.getRichTextFrameLocator().getByRole('link', { name: basename(pathToAttach) });
|
||||
if (process.platform === 'darwin') {
|
||||
await targetLink.click({ modifiers: ['Meta'] });
|
||||
} else {
|
||||
|
@@ -8,7 +8,7 @@ test.describe('settings', () => {
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Sort order buttons should be visible by default
|
||||
const sortOrderLocator = mainScreen.noteList.container.getByRole('button', { name: 'Toggle sort order' });
|
||||
const sortOrderLocator = mainScreen.noteList.sortOrderButton;
|
||||
await expect(sortOrderLocator).toBeVisible();
|
||||
|
||||
await mainScreen.openSettings(electronApp);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from './util/test';
|
||||
import { test } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import SettingsScreen from './models/SettingsScreen';
|
||||
import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
@@ -28,7 +28,7 @@ test.describe('simpleBackup', () => {
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Backups should work
|
||||
expect(await activateMainMenuItem(electronApp, 'Create backup')).toBe(true);
|
||||
await activateMainMenuItem(electronApp, 'Create backup');
|
||||
|
||||
const successDialog = mainWindow.locator('iframe[id$=backup-backupDialog]');
|
||||
await successDialog.waitFor();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
|
||||
import type { ElectronApplication } from '@playwright/test';
|
||||
import { expect, type ElectronApplication } from '@playwright/test';
|
||||
import type { MenuItem } from 'electron';
|
||||
|
||||
|
||||
@@ -7,35 +7,45 @@ import type { MenuItem } from 'electron';
|
||||
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts
|
||||
|
||||
// If given, `parentMenuLabel` should be the label of the menu containing the target item.
|
||||
const activateMainMenuItem = (
|
||||
const activateMainMenuItem = async (
|
||||
electronApp: ElectronApplication,
|
||||
targetItemLabel: string,
|
||||
targetItemLabel: string|RegExp,
|
||||
parentMenuLabel?: string,
|
||||
) => {
|
||||
return electronApp.evaluate(async ({ Menu }, [targetItemLabel, parentMenuLabel]) => {
|
||||
const activateItemInSubmenu = (submenu: MenuItem[], parentLabel: string) => {
|
||||
for (const item of submenu) {
|
||||
const matchesParent = !parentMenuLabel || parentLabel === parentMenuLabel;
|
||||
if (item.label === targetItemLabel && matchesParent && item.visible) {
|
||||
// Found!
|
||||
item.click();
|
||||
return true;
|
||||
} else if (item.submenu) {
|
||||
const foundItem = activateItemInSubmenu(item.submenu.items, item.label);
|
||||
await expect.poll(() => {
|
||||
return electronApp.evaluate(async ({ Menu }, [targetItemLabel, parentMenuLabel]) => {
|
||||
const activateItemInSubmenu = (submenu: MenuItem[], parentLabel: string) => {
|
||||
for (const item of submenu) {
|
||||
const matchesParent = !parentMenuLabel || parentLabel === parentMenuLabel;
|
||||
const matchesLabel = typeof targetItemLabel === 'string' ? (
|
||||
targetItemLabel === item.label
|
||||
) : (
|
||||
item.label.match(targetItemLabel)
|
||||
);
|
||||
|
||||
if (foundItem) {
|
||||
if (matchesLabel && matchesParent && item.visible) {
|
||||
// Found!
|
||||
item.click();
|
||||
return true;
|
||||
} else if (item.submenu) {
|
||||
const foundItem = activateItemInSubmenu(item.submenu.items, item.label);
|
||||
|
||||
if (foundItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No item found
|
||||
return false;
|
||||
};
|
||||
// No item found
|
||||
return false;
|
||||
};
|
||||
|
||||
const appMenu = Menu.getApplicationMenu();
|
||||
return activateItemInSubmenu(appMenu.items, '');
|
||||
}, [targetItemLabel, parentMenuLabel]);
|
||||
const appMenu = Menu.getApplicationMenu();
|
||||
return activateItemInSubmenu(appMenu.items, '');
|
||||
}, [targetItemLabel, parentMenuLabel]);
|
||||
}, {
|
||||
message: `should find and activate menu item with label ${JSON.stringify(targetItemLabel)}`,
|
||||
}).toBe(true);
|
||||
};
|
||||
|
||||
export default activateMainMenuItem;
|
||||
|
@@ -6,10 +6,12 @@ import createStartupArgs from './createStartupArgs';
|
||||
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
|
||||
|
||||
|
||||
type StartWithPluginsResult = { app: ElectronApplication; mainWindow: Page };
|
||||
|
||||
type JoplinFixtures = {
|
||||
profileDirectory: string;
|
||||
electronApp: ElectronApplication;
|
||||
startAppWithPlugins: (pluginPaths: string[])=> Promise<StartWithPluginsResult>;
|
||||
startupPluginsLoaded: Promise<void>;
|
||||
mainWindow: Page;
|
||||
};
|
||||
@@ -17,6 +19,20 @@ type JoplinFixtures = {
|
||||
// A custom fixture that loads an electron app. See
|
||||
// https://playwright.dev/docs/test-fixtures
|
||||
|
||||
const getAndResizeMainWindow = async (electronApp: ElectronApplication) => {
|
||||
const mainWindow = await firstNonDevToolsWindow(electronApp);
|
||||
|
||||
// Setting the viewport size helps keep test environments consistent.
|
||||
await mainWindow.setViewportSize({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
const testDir = dirname(__dirname);
|
||||
|
||||
export const test = base.extend<JoplinFixtures>({
|
||||
// Playwright fails if we don't use the object destructuring
|
||||
// pattern in the first argument.
|
||||
@@ -25,7 +41,7 @@ export const test = base.extend<JoplinFixtures>({
|
||||
//
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
profileDirectory: async ({ }, use) => {
|
||||
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
|
||||
const profilePath = resolve(join(testDir, 'test-profile'));
|
||||
const profileSubdir = join(profilePath, uuid.createNano());
|
||||
await mkdirp(profileSubdir);
|
||||
|
||||
@@ -44,6 +60,34 @@ export const test = base.extend<JoplinFixtures>({
|
||||
await electronApp.close();
|
||||
},
|
||||
|
||||
startAppWithPlugins: async ({ profileDirectory }, use) => {
|
||||
const startupArgs = createStartupArgs(profileDirectory);
|
||||
let electronApp: ElectronApplication;
|
||||
|
||||
await use(async (pluginPaths: string[]) => {
|
||||
if (electronApp) {
|
||||
throw new Error('Electron app already created');
|
||||
}
|
||||
electronApp = await electron.launch({
|
||||
args: [
|
||||
...startupArgs,
|
||||
'--dev-plugins',
|
||||
pluginPaths.map(path => resolve(testDir, path)).join(','),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
app: electronApp,
|
||||
mainWindow: await getAndResizeMainWindow(electronApp),
|
||||
};
|
||||
});
|
||||
|
||||
if (electronApp) {
|
||||
await electronApp.firstWindow();
|
||||
await electronApp.close();
|
||||
}
|
||||
},
|
||||
|
||||
startupPluginsLoaded: async ({ electronApp }, use) => {
|
||||
const startupPluginsLoadedPromise = electronApp.evaluate(({ ipcMain }) => {
|
||||
return new Promise<void>(resolve => {
|
||||
@@ -55,8 +99,7 @@ export const test = base.extend<JoplinFixtures>({
|
||||
},
|
||||
|
||||
mainWindow: async ({ electronApp }, use) => {
|
||||
const mainWindow = await firstNonDevToolsWindow(electronApp);
|
||||
await use(mainWindow);
|
||||
await use(await getAndResizeMainWindow(electronApp));
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -24,9 +24,9 @@ jest.mock('@electron/remote', () => {
|
||||
|
||||
// Import after mocking problematic libraries
|
||||
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||
const React = require('react');
|
||||
|
||||
|
||||
shimInit({ nodeSqlite: sqlite3 });
|
||||
shimInit({ nodeSqlite: sqlite3, React });
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.1.6",
|
||||
"version": "3.1.19",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -15,7 +15,7 @@
|
||||
"test": "jest",
|
||||
"test-ui": "playwright test",
|
||||
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
|
||||
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
|
||||
"modifyReleaseAssets": "node tools/modifyReleaseAssets.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -124,33 +124,34 @@
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
"devDependencies": {
|
||||
"7zip-bin": "5.2.0",
|
||||
"@electron/rebuild": "3.3.0",
|
||||
"@electron/rebuild": "3.6.0",
|
||||
"@joplin/default-plugins": "~3.1",
|
||||
"@joplin/tools": "~3.1",
|
||||
"@playwright/test": "1.43.1",
|
||||
"@playwright/test": "1.44.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "18.19.34",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.19.39",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/tesseract.js": "2.0.0",
|
||||
"axios": "^1.7.7",
|
||||
"electron": "29.4.5",
|
||||
"electron-builder": "24.13.3",
|
||||
"glob": "10.3.16",
|
||||
"glob": "10.4.5",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"js-sha512": "0.9.0",
|
||||
"nan": "2.19.0",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"ts-jest": "29.1.1",
|
||||
"ts-jest": "29.1.5",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/notarize": "2.1.0",
|
||||
"@electron/notarize": "2.3.2",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
@@ -158,7 +159,7 @@
|
||||
"@joplin/lib": "~3.1",
|
||||
"@joplin/renderer": "~3.1",
|
||||
"@joplin/utils": "~3.1",
|
||||
"@sentry/electron": "4.17.0",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@types/mustache": "4.2.5",
|
||||
"async-mutex": "0.5.0",
|
||||
"codemirror": "5.65.9",
|
||||
@@ -166,7 +167,6 @@
|
||||
"compare-versions": "6.1.0",
|
||||
"countable": "3.0.1",
|
||||
"debounce": "1.2.1",
|
||||
"electron-log": "5.1.6",
|
||||
"electron-updater": "6.2.1",
|
||||
"electron-window-state": "5.0.3",
|
||||
"formatcoords": "1.1.3",
|
||||
@@ -200,7 +200,7 @@
|
||||
"styled-components": "5.3.11",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "4.0.3",
|
||||
"tesseract.js": "5.0.5",
|
||||
"tesseract.js": "5.1.0",
|
||||
"tinymce": "5.10.6"
|
||||
}
|
||||
}
|
||||
|
@@ -2,11 +2,10 @@ import * as React from 'react';
|
||||
import { AppState } from '../app.reducer';
|
||||
import CommandService, { SearchResult as CommandSearchResult } from '@joplin/lib/services/CommandService';
|
||||
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
const { connect } = require('react-redux');
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import SearchEngine, { ComplexTerm } from '@joplin/lib/services/search/SearchEngine';
|
||||
import gotoAnythingStyleQuery from '@joplin/lib/services/search/gotoAnythingStyleQuery';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
@@ -14,7 +13,7 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import ItemList from '../gui/ItemList';
|
||||
import HelpButton from '../gui/HelpButton';
|
||||
const { surroundKeywords, nextWhitespaceIndex, removeDiacritics } = require('@joplin/lib/string-utils.js');
|
||||
import { surroundKeywords, nextWhitespaceIndex, removeDiacritics } from '@joplin/lib/string-utils';
|
||||
import { mergeOverlappingIntervals } from '@joplin/lib/ArrayUtils';
|
||||
import markupLanguageUtils from '../utils/markupLanguageUtils';
|
||||
import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand';
|
||||
@@ -23,6 +22,7 @@ import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import Dialog from '../gui/Dialog';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
|
||||
const logger = Logger.create('GotoAnything');
|
||||
|
||||
@@ -129,8 +129,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
private inputRef: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private itemListRef: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private listUpdateIID_: any;
|
||||
private listUpdateQueue_: AsyncActionQueue;
|
||||
private markupToHtml_: MarkupToHtml;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private userCallback_: any = null;
|
||||
@@ -141,6 +140,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
const startString = props?.userData?.startString ? props?.userData?.startString : '';
|
||||
|
||||
this.userCallback_ = props?.userData?.callback;
|
||||
this.listUpdateQueue_ = new AsyncActionQueue(100);
|
||||
|
||||
this.state = {
|
||||
query: startString,
|
||||
@@ -235,7 +235,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.listUpdateIID_) shim.clearTimeout(this.listUpdateIID_);
|
||||
void this.listUpdateQueue_.reset();
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'VISIBLE_DIALOGS_REMOVE',
|
||||
@@ -263,12 +263,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
public scheduleListUpdate() {
|
||||
if (this.listUpdateIID_) shim.clearTimeout(this.listUpdateIID_);
|
||||
|
||||
this.listUpdateIID_ = shim.setTimeout(async () => {
|
||||
await this.updateList();
|
||||
this.listUpdateIID_ = null;
|
||||
}, 100);
|
||||
this.listUpdateQueue_.push(() => this.updateList());
|
||||
}
|
||||
|
||||
public async keywords(searchQuery: string) {
|
||||
@@ -360,7 +355,6 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
}
|
||||
} else {
|
||||
const limit = 20;
|
||||
const searchKeywords = await this.keywords(searchQuery);
|
||||
|
||||
// Note: any filtering must be done **before** fetching the notes, because we're
|
||||
// going to apply a limit to the number of fetched notes.
|
||||
@@ -381,6 +375,10 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
results = results.filter(r => !!notesById[r.id])
|
||||
.map(r => ({ ...r, title: notesById[r.id].title }));
|
||||
|
||||
const normalizedKeywords = (await this.keywords(searchQuery)).map(
|
||||
({ valueRegex }: ComplexTerm) => new RegExp(removeDiacritics(valueRegex), 'ig'),
|
||||
);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i];
|
||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||
@@ -388,21 +386,14 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
if (row.fields.includes('body')) {
|
||||
let fragments = '...';
|
||||
|
||||
if (i < limit) { // Display note fragments of search keyword matches
|
||||
const { markupLanguage, content } = getContentMarkupLanguageAndBody(
|
||||
row,
|
||||
notesById,
|
||||
resources,
|
||||
);
|
||||
|
||||
const loadFragments = (markupLanguage: MarkupLanguage, content: string) => {
|
||||
const indices = [];
|
||||
const body = this.markupToHtml().stripMarkup(markupLanguage, content, { collapseWhiteSpaces: true });
|
||||
const normalizedBody = removeDiacritics(body);
|
||||
|
||||
// Iterate over all matches in the body for each search keyword
|
||||
for (let { valueRegex } of searchKeywords) {
|
||||
valueRegex = removeDiacritics(valueRegex);
|
||||
|
||||
for (const match of removeDiacritics(body).matchAll(new RegExp(valueRegex, 'ig'))) {
|
||||
for (const keywordRegex of normalizedKeywords) {
|
||||
for (const match of normalizedBody.matchAll(keywordRegex)) {
|
||||
// Populate 'indices' with [begin index, end index] of each note fragment
|
||||
// Begins at the regex matching index, ends at the next whitespace after seeking 15 characters to the right
|
||||
indices.push([match.index, nextWhitespaceIndex(body, match.index + match[0].length + 15)]);
|
||||
@@ -418,6 +409,19 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
fragments = mergedIndices.map((f: any) => body.slice(f[0], f[1])).join(' ... ');
|
||||
// Add trailing ellipsis if the final fragment doesn't end where the note is ending
|
||||
if (mergedIndices.length && mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...';
|
||||
};
|
||||
|
||||
if (i < limit) { // Display note fragments of search keyword matches
|
||||
const { markupLanguage, content } = getContentMarkupLanguageAndBody(
|
||||
row,
|
||||
notesById,
|
||||
resources,
|
||||
);
|
||||
|
||||
// Don't load fragments for long notes -- doing so can lead to UI freezes.
|
||||
if (content.length < 100_000) {
|
||||
loadFragments(markupLanguage, content);
|
||||
}
|
||||
}
|
||||
|
||||
results[i] = { ...row, path, fragments };
|
||||
@@ -539,11 +543,22 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
const resultId = getResultId(item);
|
||||
const isSelected = resultId === this.state.selectedItemId;
|
||||
const rowStyle = isSelected ? style.rowSelected : style.row;
|
||||
|
||||
const wrapKeywordMatches = (unescapedContent: string) => {
|
||||
return surroundKeywords(
|
||||
this.state.keywords,
|
||||
unescapedContent,
|
||||
`<span class="match-highlight" style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`,
|
||||
'</span>',
|
||||
{ escapeHtml: true },
|
||||
);
|
||||
};
|
||||
|
||||
const titleHtml = item.fragments
|
||||
? `<span style="font-weight: bold; color: ${theme.color};">${item.title}</span>`
|
||||
: surroundKeywords(this.state.keywords, item.title, `<span style="font-weight: bold; color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
|
||||
: wrapKeywordMatches(item.title);
|
||||
|
||||
const fragmentsHtml = !item.fragments ? null : surroundKeywords(this.state.keywords, item.fragments, `<span style="color: ${theme.searchMarkerColor}; background-color: ${theme.searchMarkerBackgroundColor}">`, '</span>', { escapeHtml: true });
|
||||
const fragmentsHtml = !item.fragments ? null : wrapKeywordMatches(item.fragments);
|
||||
|
||||
const folderIcon = <i style={{ fontSize: theme.fontSize, marginRight: 2 }} className="fa fa-book" role='img' aria-label={_('Notebook')} />;
|
||||
const pathComp = !item.path ? null : <div style={style.rowPath}>{folderIcon} {item.path}</div>;
|
||||
|
@@ -46,23 +46,37 @@ describe('AutoUpdaterService', () => {
|
||||
expect(release.tag_name).toBe('v3.1.2');
|
||||
});
|
||||
|
||||
it('should return the correct download URL for Windows', async () => {
|
||||
it('should return the correct download URL for Windows x32', async () => {
|
||||
const release = await service.fetchLatestRelease(true);
|
||||
expect(release).toBeDefined();
|
||||
const url = service.getDownloadUrlForPlatform(release, 'win32');
|
||||
const url = service.getDownloadUrlForPlatform(release, 'win32', 'ia32');
|
||||
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest.yml');
|
||||
});
|
||||
|
||||
it('should return the correct download URL for Mac', async () => {
|
||||
it('should return the correct download URL for Windows x64', async () => {
|
||||
const release = await service.fetchLatestRelease(true);
|
||||
expect(release).toBeDefined();
|
||||
const url = service.getDownloadUrlForPlatform(release, 'darwin');
|
||||
const url = service.getDownloadUrlForPlatform(release, 'win32', 'x64');
|
||||
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest.yml');
|
||||
});
|
||||
|
||||
it('should return the correct download URL for Mac x64', async () => {
|
||||
const release = await service.fetchLatestRelease(true);
|
||||
expect(release).toBeDefined();
|
||||
const url = service.getDownloadUrlForPlatform(release, 'darwin', 'x64');
|
||||
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac.yml');
|
||||
});
|
||||
|
||||
it('should return the correct download URL for Mac arm64', async () => {
|
||||
const release = await service.fetchLatestRelease(true);
|
||||
expect(release).toBeDefined();
|
||||
const url = service.getDownloadUrlForPlatform(release, 'darwin', 'arm64');
|
||||
expect(url).toBe('https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac-arm64.yml');
|
||||
});
|
||||
|
||||
it('should throw an error for Linux', async () => {
|
||||
const release = await service.fetchLatestRelease(true);
|
||||
expect(release).toBeDefined();
|
||||
expect(() => service.getDownloadUrlForPlatform(release, 'linux')).toThrow('The AutoUpdaterService does not support the following platform: linux');
|
||||
expect(() => service.getDownloadUrlForPlatform(release, 'linux', 'amd64')).toThrow('The AutoUpdaterService does not support the following platform: linux');
|
||||
});
|
||||
});
|
||||
|
@@ -19,16 +19,28 @@ export enum AutoUpdaterEvents {
|
||||
export const defaultUpdateInterval = 12 * 60 * 60 * 1000;
|
||||
export const initialUpdateStartup = 5 * 1000;
|
||||
const releasesLink = 'https://objects.joplinusercontent.com/r/releases';
|
||||
const supportedPlatformAssets: { [key in string]: string } = {
|
||||
'darwin': 'latest-mac.yml',
|
||||
'win32': 'latest.yml',
|
||||
export type Architecture = typeof process.arch;
|
||||
interface PlatformAssets {
|
||||
[platform: string]: {
|
||||
[arch in Architecture]?: string;
|
||||
};
|
||||
}
|
||||
const supportedPlatformAssets: PlatformAssets = {
|
||||
'darwin': {
|
||||
'x64': 'latest-mac.yml',
|
||||
'arm64': 'latest-mac-arm64.yml',
|
||||
},
|
||||
'win32': {
|
||||
'x64': 'latest.yml',
|
||||
'ia32': 'latest.yml',
|
||||
},
|
||||
};
|
||||
|
||||
export interface AutoUpdaterServiceInterface {
|
||||
checkForUpdates(): void;
|
||||
checkForUpdates(isManualCheck: boolean): void;
|
||||
updateApp(): void;
|
||||
fetchLatestRelease(includePreReleases: boolean): Promise<GitHubRelease>;
|
||||
getDownloadUrlForPlatform(release: GitHubRelease, platform: string): string;
|
||||
getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string;
|
||||
}
|
||||
|
||||
export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
@@ -36,10 +48,11 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
private logger_: LoggerWrapper;
|
||||
private devMode_: boolean;
|
||||
private enableDevMode = true; // force the updater to work in "dev" mode
|
||||
private enableAutoDownload = false; // automatically download an update when it is found
|
||||
private enableAutoDownload = true; // automatically download an update when it is found
|
||||
private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application
|
||||
private includePreReleases_ = false;
|
||||
private allowDowngrade = false;
|
||||
private isManualCheckInProgress = false;
|
||||
|
||||
public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
||||
this.window_ = mainWindow;
|
||||
@@ -49,8 +62,9 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
this.configureAutoUpdater();
|
||||
}
|
||||
|
||||
public checkForUpdates = async (): Promise<void> => {
|
||||
public checkForUpdates = async (isManualCheck = false): Promise<void> => {
|
||||
try {
|
||||
this.isManualCheckInProgress = isManualCheck;
|
||||
await this.checkForLatestRelease();
|
||||
} catch (error) {
|
||||
this.logger_.error('Failed to check for updates:', error);
|
||||
@@ -76,15 +90,20 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
};
|
||||
|
||||
|
||||
public getDownloadUrlForPlatform(release: GitHubRelease, platform: string): string {
|
||||
const assetName: string = supportedPlatformAssets[platform];
|
||||
if (!assetName) {
|
||||
public getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string {
|
||||
if (!supportedPlatformAssets[platform]) {
|
||||
throw new Error(`The AutoUpdaterService does not support the following platform: ${platform}`);
|
||||
}
|
||||
|
||||
const platformAssets = supportedPlatformAssets[platform];
|
||||
const assetName: string | undefined = platformAssets ? platformAssets[arch as Architecture] : undefined;
|
||||
if (!assetName) {
|
||||
throw new Error(`The AutoUpdaterService does not support the architecture: ${arch} for platform: ${platform}`);
|
||||
}
|
||||
|
||||
const asset: GitHubReleaseAsset = release.assets.find(a => a.name === assetName);
|
||||
if (!asset) {
|
||||
throw new Error('No suitable update asset found for this platform.');
|
||||
throw new Error(`Yml file: ${assetName} not found for version: ${release.tag_name} platform: ${platform} and architecture: ${arch}`);
|
||||
}
|
||||
|
||||
return asset.browser_download_url;
|
||||
@@ -110,11 +129,12 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
const release: GitHubRelease = await this.fetchLatestRelease(this.includePreReleases_);
|
||||
|
||||
try {
|
||||
let assetUrl = this.getDownloadUrlForPlatform(release, shim.platformName());
|
||||
let assetUrl = this.getDownloadUrlForPlatform(release, shim.platformName(), process.arch);
|
||||
// electron's autoUpdater appends automatically the platform's yml file to the link so we should remove it
|
||||
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl });
|
||||
await autoUpdater.checkForUpdates();
|
||||
this.isManualCheckInProgress = false;
|
||||
} catch (error) {
|
||||
this.logger_.error(`Update download url failed: ${error.message}`);
|
||||
}
|
||||
@@ -150,6 +170,10 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
};
|
||||
|
||||
private onUpdateNotAvailable = (_info: UpdateInfo): void => {
|
||||
if (this.isManualCheckInProgress) {
|
||||
this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable);
|
||||
}
|
||||
|
||||
this.logger_.info('Update not available.');
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useRef, useImperativeHandle, forwardRef, useEffect } from 'react';
|
||||
import { useRef, useImperativeHandle, forwardRef, useEffect, useMemo } from 'react';
|
||||
import useViewIsReady from './hooks/useViewIsReady';
|
||||
import useThemeCss from './hooks/useThemeCss';
|
||||
import useContentSize from './hooks/useContentSize';
|
||||
@@ -8,14 +8,10 @@ import useHtmlLoader from './hooks/useHtmlLoader';
|
||||
import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages';
|
||||
import useScriptLoader from './hooks/useScriptLoader';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import styled from 'styled-components';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
const logger = Logger.create('UserWebview');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
type StyleProps = any;
|
||||
|
||||
export interface Props {
|
||||
html: string;
|
||||
scripts: string[];
|
||||
@@ -36,15 +32,6 @@ export interface Props {
|
||||
onReady?: Function;
|
||||
}
|
||||
|
||||
const StyledFrame = styled.iframe<{ fitToContent: boolean; borderBottom: boolean }>`
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: ${(props: StyleProps) => props.fitToContent ? `${props.width}px` : '100%'};
|
||||
height: ${(props: StyleProps) => props.fitToContent ? `${props.height}px` : '100%'};
|
||||
border: none;
|
||||
border-bottom: ${(props: StyleProps) => props.borderBottom ? `1px solid ${props.theme.dividerColor}` : 'none'};
|
||||
`;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function serializeForm(form: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -153,15 +140,18 @@ function UserWebview(props: Props, ref: any) {
|
||||
cssFilePath,
|
||||
);
|
||||
|
||||
return <StyledFrame
|
||||
const style = useMemo(() => ({
|
||||
'--content-width': `${contentSize.width}px`,
|
||||
'--content-height': `${contentSize.height}px`,
|
||||
} as React.CSSProperties), [contentSize.width, contentSize.height]);
|
||||
|
||||
return <iframe
|
||||
id={props.viewId}
|
||||
width={contentSize.width}
|
||||
height={contentSize.height}
|
||||
fitToContent={props.fitToContent}
|
||||
style={style}
|
||||
className={`plugin-user-webview ${props.fitToContent ? '-fit-to-content' : ''} ${props.borderBottom ? '-border-bottom' : ''}`}
|
||||
ref={viewRef}
|
||||
src="services/plugins/UserWebviewIndex.html"
|
||||
borderBottom={props.borderBottom}
|
||||
></StyledFrame>;
|
||||
></iframe>;
|
||||
}
|
||||
|
||||
export default forwardRef(UserWebview);
|
||||
|
@@ -7,18 +7,12 @@ import UserWebview, { Props as UserWebviewProps } from './UserWebview';
|
||||
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Dialog from '../../gui/Dialog';
|
||||
const styled = require('styled-components').default;
|
||||
|
||||
interface Props extends UserWebviewProps {
|
||||
buttons: ButtonSpec[];
|
||||
fitToContent: boolean;
|
||||
}
|
||||
|
||||
const UserWebViewWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
function defaultButtons(): ButtonSpec[] {
|
||||
return [
|
||||
{
|
||||
@@ -84,7 +78,7 @@ export default function UserWebviewDialog(props: Props) {
|
||||
|
||||
return (
|
||||
<Dialog className={`user-webview-dialog ${props.fitToContent ? '-fit' : ''}`}>
|
||||
<UserWebViewWrapper>
|
||||
<div className='user-dialog-wrapper'>
|
||||
<UserWebview
|
||||
ref={webviewRef}
|
||||
html={props.html}
|
||||
@@ -98,7 +92,7 @@ export default function UserWebviewDialog(props: Props) {
|
||||
onDismiss={onDismiss}
|
||||
onReady={onReady}
|
||||
/>
|
||||
</UserWebViewWrapper>
|
||||
</div>
|
||||
<UserWebviewDialogButtonBar buttons={buttons}/>
|
||||
</Dialog>
|
||||
);
|
||||
|
@@ -5,20 +5,10 @@ import { ButtonSpec } from '@joplin/lib/services/plugins/api/types';
|
||||
const styled = require('styled-components').default;
|
||||
const { space } = require('styled-system');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
type StyleProps = any;
|
||||
|
||||
interface Props {
|
||||
buttons: ButtonSpec[];
|
||||
}
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: flex-end;
|
||||
padding-top: ${(props: StyleProps) => props.theme.mainPadding}px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`${space}`;
|
||||
|
||||
@@ -48,8 +38,8 @@ export default function UserWebviewDialogButtonBar(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<div className='user-dialog-button-bar'>
|
||||
{renderButtons()}
|
||||
</StyledRoot>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -51,6 +51,7 @@ const webviewApi = {
|
||||
|
||||
docReady(() => {
|
||||
const rootElement = document.createElement('div');
|
||||
rootElement.setAttribute('id', 'joplin-plugin-content-root');
|
||||
document.getElementsByTagName('body')[0].appendChild(rootElement);
|
||||
|
||||
const contentElement = document.createElement('div');
|
||||
|
3
packages/app-desktop/services/plugins/styles/index.scss
Normal file
3
packages/app-desktop/services/plugins/styles/index.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@use './plugin-user-webview.scss';
|
||||
@use './user-dialog-wrapper.scss';
|
||||
@use './user-dialog-button-bar.scss';
|
@@ -0,0 +1,17 @@
|
||||
|
||||
.plugin-user-webview {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.-border-bottom {
|
||||
border-bottom: 1px solid var(--joplin-divider-color);
|
||||
}
|
||||
|
||||
&.-fit-to-content {
|
||||
width: var(--content-width);
|
||||
height: var(--content-height);
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
|
||||
.user-dialog-button-bar {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
justify-content: flex-end;
|
||||
padding-top: var(--joplin-main-padding);
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
|
||||
.user-dialog-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
@@ -11,6 +11,7 @@
|
||||
@use 'gui/UpdateNotification/style.scss' as update-notification;
|
||||
@use 'gui/TrashNotification/style.scss' as trash-notification;
|
||||
@use 'gui/Sidebar/style.scss' as sidebar-styles;
|
||||
@use 'gui/styles/index.scss';
|
||||
@use 'gui/NoteEditor/style.scss';
|
||||
@use 'gui/NoteEditor/style.scss' as note-editor-styles;
|
||||
@use 'services/plugins/styles/index.scss' as plugins-styles;
|
||||
@use 'gui/styles/index.scss' as gui-styles;
|
||||
@use 'main.scss' as main;
|
||||
|
@@ -16,9 +16,13 @@ if [[ $NEED_COMPILING == 1 ]]; then
|
||||
echo "Copying from: $PLUGIN_PATH"
|
||||
echo "To: $TEMP_PLUGIN_PATH"
|
||||
|
||||
rsync -a --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
|
||||
rsync -a --exclude "cache/" --exclude "node_modules" --delete "$PLUGIN_PATH/" "$TEMP_PLUGIN_PATH/"
|
||||
|
||||
NODE_OPTIONS=--openssl-legacy-provider npm install --prefix="$TEMP_PLUGIN_PATH" && yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
|
||||
cd "$TEMP_PLUGIN_PATH/"
|
||||
NODE_OPTIONS=--openssl-legacy-provider npm install
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
yarn start --dev-plugins "$TEMP_PLUGIN_PATH"
|
||||
else
|
||||
yarn start --dev-plugins "$PLUGIN_PATH"
|
||||
fi
|
||||
|
69
packages/app-desktop/tools/generateLatestArm64Yml.ts
Normal file
69
packages/app-desktop/tools/generateLatestArm64Yml.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export interface GenerateInfo {
|
||||
version: string;
|
||||
dmgPath: string;
|
||||
zipPath: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const calculateHash = (filePath: string): string => {
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const hashSum = crypto.createHash('sha512');
|
||||
hashSum.update(fileBuffer);
|
||||
return hashSum.digest('base64');
|
||||
};
|
||||
|
||||
const getFileSize = (filePath: string): number => {
|
||||
return fs.statSync(filePath).size;
|
||||
};
|
||||
|
||||
export const generateLatestArm64Yml = (info: GenerateInfo, destinationPath: string): string | undefined => {
|
||||
if (!fs.existsSync(info.dmgPath) || !fs.existsSync(info.zipPath)) {
|
||||
throw new Error(`One or both executable files do not exist: ${info.dmgPath}, ${info.zipPath}`);
|
||||
}
|
||||
if (!info.version) {
|
||||
throw new Error('Version is empty');
|
||||
}
|
||||
if (!destinationPath) {
|
||||
throw new Error('Destination path is empty');
|
||||
}
|
||||
|
||||
console.info('Calculating hash of files...');
|
||||
const dmgHash: string = calculateHash(info.dmgPath);
|
||||
const zipHash: string = calculateHash(info.zipPath);
|
||||
|
||||
console.info('Calculating size of files...');
|
||||
const dmgSize: number = getFileSize(info.dmgPath);
|
||||
const zipSize: number = getFileSize(info.zipPath);
|
||||
|
||||
console.info('Generating content of latest-mac-arm64.yml file...');
|
||||
|
||||
if (!fs.existsSync(destinationPath)) {
|
||||
fs.mkdirSync(destinationPath);
|
||||
}
|
||||
|
||||
const yamlFilePath: string = path.join(destinationPath, 'latest-mac-arm64.yml');
|
||||
const yamlContent = `version: ${info.version}
|
||||
files:
|
||||
- url: ${path.basename(info.zipPath)}
|
||||
sha512: ${zipHash}
|
||||
size: ${zipSize}
|
||||
- url: ${path.basename(info.dmgPath)}
|
||||
sha512: ${dmgHash}
|
||||
size: ${dmgSize}
|
||||
path: ${path.basename(info.zipPath)}
|
||||
sha512: ${zipHash}
|
||||
releaseDate: '${info.releaseDate}'
|
||||
`;
|
||||
|
||||
fs.writeFileSync(yamlFilePath, yamlContent);
|
||||
console.log(`YML file for version ${info.version} was generated successfully at ${destinationPath} for arm64.`);
|
||||
|
||||
const fileContent: string = fs.readFileSync(yamlFilePath, 'utf8');
|
||||
console.log('Generated YML Content:\n', fileContent);
|
||||
|
||||
return yamlFilePath;
|
||||
};
|
118
packages/app-desktop/tools/githubReleasesUtils.ts
Normal file
118
packages/app-desktop/tools/githubReleasesUtils.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as fs from 'fs';
|
||||
import { createWriteStream } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import axios from 'axios';
|
||||
import { GitHubRelease, GitHubReleaseAsset } from '../utils/checkForUpdatesUtils';
|
||||
|
||||
export interface Context {
|
||||
repo: string; // {owner}/{repo}
|
||||
githubToken: string;
|
||||
targetTag: string;
|
||||
}
|
||||
|
||||
const apiBaseUrl = 'https://api.github.com/repos/';
|
||||
const defaultApiHeaders = (context: Context) => ({
|
||||
'User-Agent': 'Joplin',
|
||||
'Authorization': `token ${context.githubToken}`,
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
});
|
||||
|
||||
export const getTargetRelease = async (context: Context, targetTag: string): Promise<GitHubRelease> => {
|
||||
console.log('Fetching releases...');
|
||||
|
||||
// Note: We need to fetch all releases, not just /releases/tag/tag-name-here.
|
||||
// The latter doesn't include draft releases.
|
||||
|
||||
const result = await fetch(`${apiBaseUrl}${context.repo}/releases`, {
|
||||
method: 'GET',
|
||||
headers: defaultApiHeaders(context),
|
||||
});
|
||||
|
||||
const releases = await result.json();
|
||||
if (!result.ok) {
|
||||
throw new Error(`Error fetching release: ${JSON.stringify(releases)}`);
|
||||
}
|
||||
|
||||
for (const release of releases) {
|
||||
if (release.tag_name === targetTag) {
|
||||
return release;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No release with tag ${targetTag} found!`);
|
||||
};
|
||||
|
||||
// Download a file from Joplin Desktop releases
|
||||
export const downloadFileFromGitHub = async (context: Context, asset: GitHubReleaseAsset, destinationDir: string) => {
|
||||
const downloadPath = path.join(destinationDir, asset.name);
|
||||
if (!fs.existsSync(destinationDir)) {
|
||||
fs.mkdirSync(destinationDir);
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(`Downloading ${asset.name} from ${asset.url} to ${downloadPath}`);
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: asset.url,
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
...defaultApiHeaders(context),
|
||||
'Accept': 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`Failed to download file: Status Code ${response.status}`);
|
||||
}
|
||||
|
||||
await pipeline(response.data, createWriteStream(downloadPath));
|
||||
console.log('Download successful!');
|
||||
/* eslint-enable no-console */
|
||||
return downloadPath;
|
||||
} catch (error) {
|
||||
throw new Error('Download not successful.');
|
||||
}
|
||||
};
|
||||
|
||||
export const updateReleaseAsset = async (context: Context, assetUrl: string, newName: string) => {
|
||||
console.log('Updating asset with URL', assetUrl, 'to have name, ', newName);
|
||||
|
||||
// See https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#update-a-release-asset
|
||||
const result = await fetch(assetUrl, {
|
||||
method: 'PATCH',
|
||||
headers: defaultApiHeaders(context),
|
||||
body: JSON.stringify({
|
||||
name: newName,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(`Unable to update release asset: ${await result.text()}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadReleaseAsset = async (context: Context, release: GitHubRelease, filePath: string): Promise<void> => {
|
||||
console.log(`Uploading file from ${filePath} to release ${release.tag_name}`);
|
||||
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const fileName = path.basename(filePath);
|
||||
const uploadUrl = `https://uploads.github.com/repos/${context.repo}/releases/${release.id}/assets?name=${encodeURIComponent(fileName)}`;
|
||||
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...defaultApiHeaders(context),
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: fileContent,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload asset: ${await response.text()}`);
|
||||
} else {
|
||||
console.log(`${fileName} uploaded successfully.`);
|
||||
}
|
||||
};
|
104
packages/app-desktop/tools/modifyReleaseAssets.ts
Normal file
104
packages/app-desktop/tools/modifyReleaseAssets.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
import path = require('path');
|
||||
import { parseArgs } from 'util';
|
||||
import { Context, downloadFileFromGitHub, getTargetRelease, updateReleaseAsset, uploadReleaseAsset } from './githubReleasesUtils';
|
||||
import { GitHubRelease } from '../utils/checkForUpdatesUtils';
|
||||
import { GenerateInfo, generateLatestArm64Yml } from './generateLatestArm64Yml';
|
||||
|
||||
const basePath = path.join(__dirname, '..');
|
||||
const downloadDir = path.join(basePath, 'downloads');
|
||||
|
||||
// Renames release assets in Joplin Desktop releases
|
||||
const renameReleaseAssets = async (context: Context, release: GitHubRelease) => {
|
||||
// Patterns used to rename releases
|
||||
const renamePatterns: [RegExp, string][] = [
|
||||
[/-arm64\.dmg$/, '-arm64.DMG'],
|
||||
];
|
||||
|
||||
for (const asset of release.assets) {
|
||||
for (const [pattern, replacement] of renamePatterns) {
|
||||
if (asset.name.match(pattern)) {
|
||||
const newName = asset.name.replace(pattern, replacement);
|
||||
await updateReleaseAsset(context, asset.url, newName);
|
||||
asset.name = newName;
|
||||
|
||||
// Only rename a release once.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Creates release assets in Joplin Desktop releases
|
||||
const createReleaseAssets = async (context: Context, release: GitHubRelease) => {
|
||||
// Create latest-mac-arm64.yml file and publish
|
||||
let dmgPath: string;
|
||||
let zipPath: string;
|
||||
for (const asset of release.assets) {
|
||||
console.log(`Checking asset: ${asset.name}`);
|
||||
|
||||
if (asset.name.endsWith('arm64.zip')) {
|
||||
zipPath = await downloadFileFromGitHub(context, asset, downloadDir);
|
||||
} else if (asset.name.endsWith('arm64.DMG')) {
|
||||
dmgPath = await downloadFileFromGitHub(context, asset, downloadDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!zipPath || !dmgPath) {
|
||||
const formattedAssets = release.assets.map(asset => ({
|
||||
name: asset.name,
|
||||
url: asset.url,
|
||||
}));
|
||||
throw new Error(`Zip path: ${zipPath} and/or dmg path: ${dmgPath} are not defined. Logging assets of release: ${JSON.stringify(formattedAssets, null, 2)}`);
|
||||
}
|
||||
|
||||
const info: GenerateInfo = {
|
||||
version: release.tag_name.slice(1),
|
||||
dmgPath: dmgPath,
|
||||
zipPath: zipPath,
|
||||
releaseDate: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const latestArm64FilePath = generateLatestArm64Yml(info, downloadDir);
|
||||
await uploadReleaseAsset(context, release, latestArm64FilePath);
|
||||
};
|
||||
|
||||
|
||||
const modifyReleaseAssets = async () => {
|
||||
const args = parseArgs({
|
||||
options: {
|
||||
tag: { type: 'string' },
|
||||
token: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!args.values.tag || !args.values.token || !args.values.repo) {
|
||||
throw new Error([
|
||||
'Required arguments: --tag, --token, --repo',
|
||||
' --tag should be a git tag with an associated release (e.g. v12.12.12)',
|
||||
' --token should be a GitHub API token',
|
||||
' --repo should be a string in the form user/reponame (e.g. laurent22/joplin)',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
const context: Context = {
|
||||
repo: args.values.repo,
|
||||
githubToken: args.values.token,
|
||||
targetTag: args.values.tag,
|
||||
};
|
||||
|
||||
const release = await getTargetRelease(context, context.targetTag);
|
||||
|
||||
if (!release.assets) {
|
||||
console.log(release);
|
||||
throw new Error(`Release ${release.tag_name} missing assets!`);
|
||||
}
|
||||
|
||||
console.log('Renaming release assets for tag', context.targetTag, context.repo);
|
||||
await renameReleaseAssets(context, release);
|
||||
console.log('Creating latest-mac-arm64.yml asset for tag', context.targetTag, context.repo);
|
||||
await createReleaseAssets(context, release);
|
||||
};
|
||||
|
||||
void modifyReleaseAssets();
|
@@ -1,109 +0,0 @@
|
||||
import { parseArgs } from 'util';
|
||||
|
||||
interface Context {
|
||||
repo: string; // {owner}/{repo}
|
||||
githubToken: string;
|
||||
}
|
||||
|
||||
const apiBaseUrl = 'https://api.github.com/repos/';
|
||||
const defaultApiHeaders = (context: Context) => ({
|
||||
'Authorization': `token ${context.githubToken}`,
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
});
|
||||
|
||||
const getTargetRelease = async (context: Context, targetTag: string) => {
|
||||
console.log('Fetching releases...');
|
||||
|
||||
// Note: We need to fetch all releases, not just /releases/tag/tag-name-here.
|
||||
// The latter doesn't include draft releases.
|
||||
|
||||
const result = await fetch(`${apiBaseUrl}${context.repo}/releases`, {
|
||||
method: 'GET',
|
||||
headers: defaultApiHeaders(context),
|
||||
});
|
||||
|
||||
const releases = await result.json();
|
||||
if (!result.ok) {
|
||||
throw new Error(`Error fetching release: ${JSON.stringify(releases)}`);
|
||||
}
|
||||
|
||||
for (const release of releases) {
|
||||
if (release.tag_name === targetTag) {
|
||||
return release;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`No release with tag ${targetTag} found!`);
|
||||
};
|
||||
|
||||
const updateReleaseAsset = async (context: Context, assetUrl: string, newName: string) => {
|
||||
console.log('Updating asset with URL', assetUrl, 'to have name, ', newName);
|
||||
|
||||
// See https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28#update-a-release-asset
|
||||
const result = await fetch(assetUrl, {
|
||||
method: 'PATCH',
|
||||
headers: defaultApiHeaders(context),
|
||||
body: JSON.stringify({
|
||||
name: newName,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(`Unable to update release asset: ${await result.text()}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Renames release assets in Joplin Desktop releases
|
||||
const renameReleaseAssets = async () => {
|
||||
const args = parseArgs({
|
||||
options: {
|
||||
tag: { type: 'string' },
|
||||
token: { type: 'string' },
|
||||
repo: { type: 'string' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!args.values.tag || !args.values.token || !args.values.repo) {
|
||||
throw new Error([
|
||||
'Required arguments: --tag, --token, --repo',
|
||||
' --tag should be a git tag with an associated release (e.g. v12.12.12)',
|
||||
' --token should be a GitHub API token',
|
||||
' --repo should be a string in the form user/reponame (e.g. laurent22/joplin)',
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
|
||||
const context: Context = {
|
||||
repo: args.values.repo,
|
||||
githubToken: args.values.token,
|
||||
};
|
||||
|
||||
console.log('Renaming release assets for tag', args.values.tag, context.repo);
|
||||
|
||||
const release = await getTargetRelease(context, args.values.tag);
|
||||
|
||||
if (!release.assets) {
|
||||
console.log(release);
|
||||
throw new Error(`Release ${release.name} missing assets!`);
|
||||
}
|
||||
|
||||
// Patterns used to rename releases
|
||||
const renamePatterns = [
|
||||
[/-arm64\.dmg$/, '-arm64.DMG'],
|
||||
];
|
||||
|
||||
for (const asset of release.assets) {
|
||||
for (const [pattern, replacement] of renamePatterns) {
|
||||
if (asset.name.match(pattern)) {
|
||||
const newName = asset.name.replace(pattern, replacement);
|
||||
await updateReleaseAsset(context, asset.url, newName);
|
||||
|
||||
// Only rename a release once.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void renameReleaseAssets();
|
@@ -7,9 +7,11 @@ export interface CheckForUpdateOptions {
|
||||
export interface GitHubReleaseAsset {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface GitHubRelease {
|
||||
id?: string;
|
||||
tag_name: string;
|
||||
prerelease: boolean;
|
||||
body: string;
|
||||
|
@@ -5717,6 +5717,41 @@ export const releases3: any = [
|
||||
'updated_at': '2024-08-17T12:40:54Z',
|
||||
'browser_download_url': 'https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac.yml',
|
||||
},
|
||||
{
|
||||
'url': 'https://api.github.com/repos/laurent22/joplin/releases/assets/186557908',
|
||||
'id': 186557908,
|
||||
'node_id': 'RA_kwDOBLftOs4LHqXU',
|
||||
'name': 'latest-mac-arm64.yml',
|
||||
'label': '',
|
||||
'uploader':
|
||||
{
|
||||
'login': 'laurent22',
|
||||
'id': 1285584,
|
||||
'node_id': 'MDQ6VXNlcjEyODU1ODQ=',
|
||||
'avatar_url': 'https://avatars.githubusercontent.com/u/1285584?v=4',
|
||||
'gravatar_id': '',
|
||||
'url': 'https://api.github.com/users/laurent22',
|
||||
'html_url': 'https://github.com/laurent22',
|
||||
'followers_url': 'https://api.github.com/users/laurent22/followers',
|
||||
'following_url': 'https://api.github.com/users/laurent22/following{/other_user}',
|
||||
'gists_url': 'https://api.github.com/users/laurent22/gists{/gist_id}',
|
||||
'starred_url': 'https://api.github.com/users/laurent22/starred{/owner}{/repo}',
|
||||
'subscriptions_url': 'https://api.github.com/users/laurent22/subscriptions',
|
||||
'organizations_url': 'https://api.github.com/users/laurent22/orgs',
|
||||
'repos_url': 'https://api.github.com/users/laurent22/repos',
|
||||
'events_url': 'https://api.github.com/users/laurent22/events{/privacy}',
|
||||
'received_events_url': 'https://api.github.com/users/laurent22/received_events',
|
||||
'type': 'User',
|
||||
'site_admin': false,
|
||||
},
|
||||
'content_type': 'text/yaml',
|
||||
'state': 'uploaded',
|
||||
'size': 484,
|
||||
'download_count': 9,
|
||||
'created_at': '2024-08-17T12:40:54Z',
|
||||
'updated_at': '2024-08-17T12:40:54Z',
|
||||
'browser_download_url': 'https://github.com/laurent22/joplin/releases/download/v3.1.3/latest-mac-arm64.yml',
|
||||
},
|
||||
{
|
||||
'url': 'https://api.github.com/repos/laurent22/joplin/releases/assets/186555028',
|
||||
'id': 186555028,
|
||||
|
@@ -125,6 +125,11 @@ const handleRangeRequest = async (request: Request, targetPath: string) => {
|
||||
// TODO: Use Logger.create (doesn't work for now because Logger is only initialized
|
||||
// in the main process.)
|
||||
const handleCustomProtocols = (logger: LoggerWrapper): CustomProtocolHandler => {
|
||||
logger = {
|
||||
...logger,
|
||||
debug: () => {},
|
||||
};
|
||||
|
||||
const readableDirectories: string[] = [];
|
||||
const readableFiles = new Map<string, number>();
|
||||
|
||||
|
@@ -79,8 +79,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097751
|
||||
versionName "3.1.3"
|
||||
versionCode 2097754
|
||||
versionName "3.1.6"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ const showResource = async (item: ResourceEntity) => {
|
||||
if (shim.mobilePlatform() === 'web') {
|
||||
const url = URL.createObjectURL(await shim.fsDriver().fileAtPath(resourcePath));
|
||||
const w = window.open(url, '_blank');
|
||||
w.addEventListener('close', () => {
|
||||
w?.addEventListener('close', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, { once: true });
|
||||
} else {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
const { BackButtonService } = require('../services/back-button.js');
|
||||
import BackButtonService from '../services/BackButtonService';
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
||||
export default class BackButtonDialogBox extends DialogBox {
|
||||
|
67
packages/app-mobile/components/Checkbox.tsx
Normal file
67
packages/app-mobile/components/Checkbox.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { TouchableHighlight, StyleSheet, TextStyle } from 'react-native';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
accessibilityLabel?: string;
|
||||
onChange?: (checked: boolean)=> void;
|
||||
style?: TextStyle;
|
||||
iconStyle?: TextStyle;
|
||||
}
|
||||
|
||||
const useStyles = (baseStyles: TextStyle|undefined, iconStyle: TextStyle|undefined) => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
...(baseStyles ?? {}),
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
height: 22,
|
||||
color: baseStyles?.color,
|
||||
...iconStyle,
|
||||
},
|
||||
});
|
||||
}, [baseStyles, iconStyle]);
|
||||
};
|
||||
|
||||
const Checkbox: React.FC<Props> = props => {
|
||||
const [checked, setChecked] = useState(props.checked);
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(props.checked);
|
||||
}, [props.checked]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
setChecked(checked => {
|
||||
const newChecked = !checked;
|
||||
props.onChange?.(newChecked);
|
||||
return newChecked;
|
||||
});
|
||||
}, [props.onChange]);
|
||||
|
||||
const iconName = checked ? 'checkbox-outline' : 'square-outline';
|
||||
const styles = useStyles(props.style, props.iconStyle);
|
||||
|
||||
const accessibilityState = useMemo(() => ({
|
||||
checked,
|
||||
}), [checked]);
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={onPress}
|
||||
style={styles.container}
|
||||
accessibilityRole="checkbox"
|
||||
accessibilityState={accessibilityState}
|
||||
accessibilityLabel={props.accessibilityLabel ?? ''}
|
||||
>
|
||||
<Icon name={iconName} style={styles.icon} />
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
@@ -78,6 +78,17 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
private onCloseList = () => {
|
||||
this.setState({ listVisible: false });
|
||||
};
|
||||
private onListLoad = (listRef: FlatList|null) => {
|
||||
if (!listRef) return;
|
||||
|
||||
for (let i = 0; i < this.props.items.length; i++) {
|
||||
const item = this.props.items[i];
|
||||
if (item.value === this.props.selectedValue) {
|
||||
listRef.scrollToIndex({ index: i, animated: false });
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
const items = this.props.items;
|
||||
@@ -228,6 +239,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
accessibilityRole='menu'
|
||||
style={wrapperStyle}>
|
||||
<FlatList
|
||||
ref={this.onListLoad}
|
||||
style={itemListStyle}
|
||||
data={this.props.items}
|
||||
renderItem={itemRenderer}
|
||||
|
@@ -15,7 +15,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const dom = useMemo(() => {
|
||||
// Note: Adding `runScripts: 'dangerously'` to allow running inline <script></script>s.
|
||||
// Use with caution.
|
||||
return new JSDOM(props.html, { runScripts: 'dangerously' });
|
||||
return new JSDOM(props.html, { runScripts: 'dangerously', pretendToBeVisual: true });
|
||||
}, [props.html]);
|
||||
|
||||
useImperativeHandle(ref, (): WebViewControl => {
|
||||
@@ -46,6 +46,25 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
injectedJavaScriptRef.current = props.injectedJavaScript;
|
||||
|
||||
useEffect(() => {
|
||||
// JSDOM polyfills
|
||||
dom.window.eval(`
|
||||
// Prevents the CodeMirror error "getClientRects is undefined".
|
||||
// See https://github.com/jsdom/jsdom/issues/3002#issue-652790925
|
||||
document.createRange = () => {
|
||||
const range = new Range();
|
||||
range.getBoundingClientRect = () => {};
|
||||
range.getClientRects = () => {
|
||||
return {
|
||||
length: 0,
|
||||
item: () => null,
|
||||
[Symbol.iterator]: () => {},
|
||||
};
|
||||
};
|
||||
|
||||
return range;
|
||||
};
|
||||
`);
|
||||
|
||||
dom.window.eval(`
|
||||
window.setWebViewApi = (api) => {
|
||||
window.ReactNativeWebView = api;
|
||||
@@ -74,7 +93,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
}, [dom]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- HACK: Allow wrapper testing logic to access the DOM.
|
||||
const additionalProps: any = { document: dom?.window?.document };
|
||||
const additionalProps: any = { window: dom?.window };
|
||||
return (
|
||||
<View style={props.style} testID={props.testID} {...additionalProps}/>
|
||||
);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
export type HandleMessageCallback = (message: string)=> void;
|
||||
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
|
||||
@@ -12,6 +13,8 @@ interface MessageCallbacks {
|
||||
onCheckboxChange: HandleMessageCallback;
|
||||
}
|
||||
|
||||
const logger = Logger.create('useOnMessage');
|
||||
|
||||
export default function useOnMessage(
|
||||
noteBody: string,
|
||||
callbacks: MessageCallbacks,
|
||||
@@ -29,10 +32,10 @@ export default function useOnMessage(
|
||||
return useCallback((msg: string) => {
|
||||
const isScrollMessage = msg.startsWith('onscroll:');
|
||||
|
||||
// Scroll messages are very frequent so we avoid logging them.
|
||||
// Scroll messages are very frequent so we avoid logging them, even
|
||||
// in debug mode
|
||||
if (!isScrollMessage) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('Got IPC message: ', msg);
|
||||
logger.debug('Got IPC message: ', msg);
|
||||
}
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
|
@@ -132,7 +132,7 @@ const EditLinkDialog = (props: LinkDialogProps) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animationType="slide"
|
||||
animationType="fade"
|
||||
containerStyle={styles.modalContent}
|
||||
transparent={true}
|
||||
visible={props.visible}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
const React = require('react');
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
@@ -18,7 +18,7 @@ import { OnMessageEvent } from '../../ExtendedWebView/types';
|
||||
|
||||
const logger = Logger.create('ImageEditor');
|
||||
|
||||
type OnSaveCallback = (svgData: string)=> void;
|
||||
type OnSaveCallback = (svgData: string)=> Promise<void>;
|
||||
type OnCancelCallback = ()=> void;
|
||||
|
||||
interface Props {
|
||||
@@ -231,6 +231,15 @@ const ImageEditor = (props: Props) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const saveThenClose = (drawing) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
JSON.stringify({
|
||||
action: 'save-and-close',
|
||||
data: drawing.outerHTML,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
if (window.editorControl === undefined) {
|
||||
${shim.injectedJs('svgEditorBundle')}
|
||||
@@ -239,6 +248,7 @@ const ImageEditor = (props: Props) => {
|
||||
{
|
||||
saveDrawing,
|
||||
closeEditor,
|
||||
saveThenClose,
|
||||
updateEditorTemplate,
|
||||
setImageHasChanges,
|
||||
},
|
||||
@@ -308,13 +318,16 @@ const ImageEditor = (props: Props) => {
|
||||
const json = JSON.parse(data);
|
||||
if (json.action === 'save') {
|
||||
await clearAutosave();
|
||||
props.onSave(json.data);
|
||||
await props.onSave(json.data);
|
||||
} else if (json.action === 'autosave') {
|
||||
await writeAutosave(json.data);
|
||||
} else if (json.action === 'save-toolbar') {
|
||||
Setting.setValue('imageeditor.jsdrawToolbar', json.data);
|
||||
} else if (json.action === 'close') {
|
||||
onRequestCloseEditor(json.promptIfUnsaved);
|
||||
} else if (json.action === 'save-and-close') {
|
||||
await props.onSave(json.data);
|
||||
onRequestCloseEditor(json.promptIfUnsaved);
|
||||
} else if (json.action === 'ready-to-load-data') {
|
||||
void onReadyToLoadData();
|
||||
} else if (json.action === 'set-image-has-changes') {
|
||||
|
@@ -22,6 +22,7 @@ const createEditorWithCallbacks = (callbacks: Partial<ImageEditorCallbacks>) =>
|
||||
|
||||
const allCallbacks: ImageEditorCallbacks = {
|
||||
saveDrawing: () => {},
|
||||
saveThenClose: ()=> {},
|
||||
closeEditor: ()=> {},
|
||||
setImageHasChanges: ()=> {},
|
||||
updateEditorTemplate: ()=> {},
|
||||
|
@@ -91,11 +91,15 @@ export const createJsDrawEditor = (
|
||||
}
|
||||
};
|
||||
|
||||
const saveNow = () => {
|
||||
callbacks.saveDrawing(editor.toSVG({
|
||||
const getEditorSVG = () => {
|
||||
return editor.toSVG({
|
||||
// Grow small images to this minimum size
|
||||
minDimension: 50,
|
||||
}), false);
|
||||
});
|
||||
};
|
||||
|
||||
const saveNow = () => {
|
||||
callbacks.saveDrawing(getEditorSVG(), false);
|
||||
|
||||
// The image is now up-to-date with the resource
|
||||
setImageHasChanges(false);
|
||||
@@ -177,13 +181,7 @@ export const createJsDrawEditor = (
|
||||
},
|
||||
saveNow,
|
||||
saveThenExit: async () => {
|
||||
saveNow();
|
||||
|
||||
// Don't show a confirmation dialog -- it's possible that
|
||||
// the code outside of the WebView still thinks changes haven't
|
||||
// been saved:
|
||||
const showConfirmation = false;
|
||||
callbacks.closeEditor(showConfirmation);
|
||||
callbacks.saveThenClose(getEditorSVG());
|
||||
},
|
||||
};
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user