1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-20 23:30:05 +02:00

Compare commits

..

83 Commits

Author SHA1 Message Date
Laurent Cozic
e80db6afb5 Server v3.5.2 2025-12-19 21:28:55 +00:00
renovate[bot]
6a06922633 Update dependency @playwright/test to v1.55.0 (#13945)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-12-19 17:36:07 +00:00
renovate[bot]
fd02d88739 Update dependency node-gyp to v11.4.2 (#13953)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 22:56:07 +00:00
renovate[bot]
dacd460f64 Update dependency node-gyp to v11.4.0 (#13950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 19:41:16 +00:00
Henry Heino
3279485f44 Mobile: Fixes #13081: Rich Text Editor: Fix checklists saved with extra space (#13951) 2025-12-18 19:41:03 +00:00
Henry Heino
eaf8d15be7 Mobile: Rich Text Editor: Set the default math/code block content to the selection (#13952) 2025-12-18 19:40:48 +00:00
Henry Heino
6b186b965a Chore: Fix CI (#13948) 2025-12-18 17:56:09 +00:00
cedecode
7a8ac14c99 All: Translation: Update de_DE.po (#13937) 2025-12-18 12:12:53 -05:00
renovate[bot]
73291fa355 Update dependency mermaid to v11.10.1 (#13930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 02:41:28 +00:00
Henry Heino
27ff8be432 Desktop: OneNote import: Fix certain embedded files are positioned under the header (#13898)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-12-18 01:12:35 +00:00
Linkosred
0904838311 Docs: Add video tutorial link for publish notes documentation (#13902) 2025-12-18 01:12:27 +00:00
Henry Heino
2798cc6027 Mobile: Fixes #13854: Fix some icons are invisible: Upgrade react-native-vector-icons to v12 (#13905) 2025-12-18 01:12:14 +00:00
Linkosred
1ede5bc499 Docs: Add video tutorial link for importing and exporting documentation (#13914) 2025-12-18 01:11:58 +00:00
Henry Heino
418a660a66 Chore: Allow specifying a custom API key at build time (#13917) 2025-12-18 01:11:29 +00:00
Linkosred
5bc073e888 Docs: Add section and video tutorial link about Rich text editor on mobile for Rich text documentation (#13921) 2025-12-18 01:10:50 +00:00
Henry Heino
87b443e051 Mobile: Accessibility: Dark mode: Improve contrast of conflicts notebook title, error messages in "Logs" (#13925) 2025-12-18 01:09:47 +00:00
Henry Heino
8e36644068 Desktop: OneNote importer: Add partial support for importing internal links (#13926) 2025-12-18 01:09:30 +00:00
Henry Heino
1833de789a Desktop: Fix search markers vanish when moving focus to a secondary window (#13927) 2025-12-18 01:09:09 +00:00
Henry Heino
0b18fd988b Desktop: Editor plugins: Fix error logged when pressing enter and a plugin-created input is focused (#13932) 2025-12-18 00:55:34 +00:00
Henry Heino
2ce65b9315 Clipper: Support importing math from Wikipedia and other websites (#13934) 2025-12-18 00:54:58 +00:00
renovate[bot]
8f4f0ee321 Update dependency sharp to v0.34.4 (#13923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 10:25:11 +00:00
renovate[bot]
6a83cc95ee Update dependency mermaid to v11.10.0 (#13929)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 10:24:42 +00:00
renovate[bot]
5134b63075 Update dependency esbuild to v0.25.10 (#13924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 21:43:32 +00:00
renovate[bot]
74527d7006 Update dependency dompurify to v3.2.7 (#13922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 19:43:18 +00:00
renovate[bot]
ad909ac6f0 Update dependency @types/serviceworker to v0.0.153 (#13919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 08:55:40 +00:00
summoner
5ff0285b85 ALL: Translation: Update hu_HU.po (#13915) 2025-12-15 13:11:08 -05:00
renovate[bot]
bcb509a965 Update dependency @react-native-documents/picker to v10.1.6 (#13911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 11:00:15 +00:00
renovate[bot]
075c98175e Update dependency fs-extra to v11.3.2 (#13910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 00:48:20 +00:00
Joplin Bot
212112d4b6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-12-14 18:38:22 +00:00
renovate[bot]
74bf0cb655 Update dependency @react-native-community/datetimepicker to v8.4.5 (#13900)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-14 17:46:57 +00:00
Laurent Cozic
b2bdf84f06 Add 'yargs' to renovate.json5 dependencies 2025-12-14 15:36:37 +00:00
Laurent Cozic
a2156a0548 Add yargs-parser to renovate.json5 dependencies 2025-12-14 14:41:48 +00:00
Laurent Cozic
620afdaab1 Android 3.5.3 2025-12-14 13:48:19 +00:00
Laurent Cozic
3f8928000e Chore: Clean up Android release script 2025-12-14 13:37:22 +00:00
Laurent Cozic
5caec161f2 Mobile: Add a link to the list of open-source licenses 2025-12-13 00:03:26 +00:00
Laurent Cozic
daab2223e7 Chore: Fix CI 2025-12-12 16:46:53 +00:00
Laurent Cozic
f96071870c All: Fixes #12172: Markdown import incorrectly parses a link as a file path 2025-12-12 14:53:35 +00:00
Laurent Cozic
5e08abb7a9 Desktop: Fixes #12367: When using RTE, switching to a note from go to anything search results with keyboard immediately updates note last modified date 2025-12-12 14:31:50 +00:00
Laurent Cozic
2c71557d88 All: Fixes #12770: Import Error: Note date incorrect when import notes with import MD - Markdown + Front Matter 2025-12-12 12:35:38 +00:00
Laurent Cozic
d551963669 All: Fixes #13008: Importing MD + frontmatter fails on empty variable 2025-12-12 11:53:03 +00:00
Laurent Cozic
7dae90c9f3 Linux: Fixes #13038: Do not suggest downgrading the app when a version has been unpublished 2025-12-12 11:29:40 +00:00
Laurent Cozic
46820fb21b Server: Fixes #13059: Confusing error message if a published note has not been synced to the server 2025-12-12 10:56:22 +00:00
Laurent Cozic
a18e49ab54 Chore: Fixes #13358: Fix randomly failing test on app/command-rmnote.test 2025-12-12 10:27:25 +00:00
Laurent Cozic
2c6eaca442 Desktop: Fixes #13814: Remove architecture warning on Windows ARM with Apple silicon 2025-12-12 10:05:37 +00:00
Laurent Cozic
44de1246d9 Desktop: Fixes #13880: Warning logged on startup when ABC Sheet Music plugin is not installed 2025-12-12 09:49:57 +00:00
renovate[bot]
ab3a0ab69f Update dependency pg-boss to v10.3.3 (#13894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 04:13:14 +00:00
Laurent Cozic
896f0e0bc5 Doc: Clarify difference between JS and JSB 2025-12-11 18:29:56 +00:00
Henry Heino
e2c933db82 Chore: Resolves #13866: Create a tool that lists dependencies of a package and its licenses (#13874) 2025-12-11 15:16:41 +00:00
Henry Heino
30c5031611 Mobile: Rich Text Editor: Fix table delete row/delete column buttons can't remove the last row/column from a table (#13877) 2025-12-11 15:16:13 +00:00
Henry Heino
e7f14a0995 Desktop: Fixes #13872: Fix importing HTML links with multi-line titles as Markdown (#13876) 2025-12-11 15:15:28 +00:00
Henry Heino
319bf79bc1 Mobile: Upgrade js-draw to v1.32.0 (#13875) 2025-12-11 08:17:37 +00:00
renovate[bot]
02f94adb96 Update dependency nodejs to v24.5.0 (#13890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 16:25:17 +00:00
Laurent Cozic
2370c12129 Revert "Update dependency @react-native/babel-preset to v0.81.0" (#13888) 2025-12-10 12:56:15 +00:00
Laurent Cozic
8d074a563b Update renovate.json5 2025-12-10 12:55:48 +00:00
renovate[bot]
1014edfdeb Update dependency @react-native/babel-preset to v0.81.0 (#13886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-10 12:54:43 +00:00
Laurent Cozic
364bdd9bb0 Chore: Allow specifying a custom API key at build time 2025-12-09 15:33:07 +00:00
Laurent Cozic
8d6b219191 lock file 2025-12-09 15:07:45 +00:00
Laurent Cozic
2455245f86 Chore: Allow creating a custom build of the Android app 2025-12-09 15:07:28 +00:00
Laurent Cozic
c669a3986e Tools: Allow generating Android images using generate-imgae script 2025-12-09 10:53:57 +00:00
Laurent Cozic
5f1a1e50d9 Doc: Add forum background images 2025-12-09 00:06:48 +00:00
Self Not Found
819a591cc0 Desktop: Add CJK characters counter in statistics panel (#13840) 2025-12-08 18:52:09 +00:00
mrjo118
421b82c86d Mobile: Add the ability to rename and delete tags (#13731) 2025-12-08 10:15:34 +00:00
Henry Heino
16169b2780 Server: Periodically delete old backups for archived accounts (#13741) 2025-12-08 10:15:18 +00:00
Charlie Arehart
49ed4ae920 Docs: add links to clarify "plugins" references (#13763)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-12-08 10:11:26 +00:00
mrjo118
13777d261c Desktop: Replace the edit profile config menu option with a gui to manage profiles (#13771) 2025-12-08 10:08:58 +00:00
Henry Heino
1c7b0e6266 Mac: Fixes #13214: Markdown editor: Don't open links on ctrl-click (#13792) 2025-12-08 10:07:41 +00:00
Henry Heino
4589670126 Chore: Server: Debug: Add debug benchmarkDeltaPerformance API (#13801) 2025-12-08 10:07:25 +00:00
marph91
b6ab6e0b46 Desktop: Use the "--no-sandbox" flag for Tuxedo OS (#13810) 2025-12-08 10:06:42 +00:00
mrjo118
9b28b618bb Desktop, Mobile: Do no re-use the 'Restored Notes' folder if it is trashed (#13813) 2025-12-08 10:06:25 +00:00
Bartolomeo
bf7cc6be03 Desktop: Resolves: #13804: Change search Resources feature to case insensitive (#13824) 2025-12-08 10:04:31 +00:00
mrjo118
e5e5b342a7 Mobile: Fixes #13825: Fix incompatible plugins cannot be uninstalled (#13828) 2025-12-08 10:03:23 +00:00
Henry Heino
9709721a73 Desktop: OneNote importer: Fix missing content in imported notebooks, improve math formula import (#13829) 2025-12-08 09:59:58 +00:00
Henry Heino
a34010ef62 Chore: Tests: Make renderBlockImages.test.ts less likely to fail in CI (#13835) 2025-12-08 09:59:50 +00:00
Henry Heino
9a6043e6a6 Chore: Desktop: Migrate the "Share note" dialog to RSCSS (#13842) 2025-12-08 09:53:15 +00:00
Henry Heino
992bf683c4 Chore: Server: Create more realistic test data (#13843) 2025-12-08 09:53:05 +00:00
Henry Heino
b40c2b8a41 Desktop: Fixes #13844: OneNote importer: Fix wrong page version imported (#13850) 2025-12-08 09:52:51 +00:00
Henry Heino
8dcd08e21d Server: Ensure that shared items are processed in the correct order (#13858) 2025-12-08 09:52:39 +00:00
mrjo118
cb2b32520d Mobile: Prevent opening the edit / delete dialog when long pressing the conflicts notebook (#13860) 2025-12-08 09:51:12 +00:00
Henry Heino
315b1d8275 Desktop: Markdown Editor: Collapse selection to a single cursor when pressing "escape" (#13864) 2025-12-08 09:49:50 +00:00
renovate[bot]
8018f1269a Update dependency react-native-share to v12.2.0 (#13861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 10:17:21 +00:00
renovate[bot]
c2d186188b Update dependency react-native-safe-area-context to v5.6.1 (#13868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 11:05:56 +00:00
renovate[bot]
d5798e558b Update dependency react-native-safe-area-context to v5.6.0 (#13865)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 09:37:17 +00:00
renovate[bot]
224bcd54f1 Update dependency fs-extra to v11.3.1 (#13849)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 23:45:31 +00:00
187 changed files with 5788 additions and 1249 deletions

View File

@@ -181,6 +181,7 @@ packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
packages/app-desktop/commands/startExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/switchProfile.js
@@ -390,6 +391,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -1028,6 +1030,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
@@ -1152,6 +1155,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
packages/editor/ProseMirror/utils/forEachHeading.js
packages/editor/ProseMirror/utils/getTextBetween.js
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
packages/editor/ProseMirror/utils/jumpToHash.js
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
@@ -1853,6 +1857,7 @@ packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js
packages/tools/git-changelog.js
packages/tools/licenses/buildReport.js
packages/tools/licenses/getLicenses.js
packages/tools/licenses/licenseChecker.js
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js

5
.gitignore vendored
View File

@@ -153,6 +153,7 @@ packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
packages/app-desktop/commands/startExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/switchProfile.js
@@ -362,6 +363,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
@@ -1000,6 +1002,7 @@ packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
packages/editor/CodeMirror/extensions/editorSettingsExtension.js
packages/editor/CodeMirror/extensions/highlightActiveLineExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
@@ -1124,6 +1127,7 @@ packages/editor/ProseMirror/utils/dom/showModal.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
packages/editor/ProseMirror/utils/forEachHeading.js
packages/editor/ProseMirror/utils/getTextBetween.js
packages/editor/ProseMirror/utils/insertRenderedMarkdown.js
packages/editor/ProseMirror/utils/jumpToHash.js
packages/editor/ProseMirror/utils/makeLinksClickableInElement.js
@@ -1825,6 +1829,7 @@ packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js
packages/tools/git-changelog.js
packages/tools/licenses/buildReport.js
packages/tools/licenses/getLicenses.js
packages/tools/licenses/licenseChecker.js
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -198,12 +198,21 @@ else
fi
# Check if it's in the latest version
if [[ -e "${INSTALL_DIR}/VERSION" ]] && [[ $(< "${INSTALL_DIR}/VERSION") == "${RELEASE_VERSION}" ]]; then
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
if [[ -e "${INSTALL_DIR}/VERSION" ]]; then
CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
VERSION_COMPARISON=$(compareVersions "$CURRENT_VERSION" "$RELEASE_VERSION")
if [[ "$VERSION_COMPARISON" == "0" ]]; then
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
elif [[ "$VERSION_COMPARISON" == "1" ]]; then
print "${COLOR_YELLOW}You have version ${CURRENT_VERSION} installed, which is newer than the latest published version ${RELEASE_VERSION}.${COLOR_RESET}"
print "${COLOR_YELLOW}Skipping installation to avoid downgrade.${COLOR_RESET}"
else
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION} installed."
fi
else
[[ -e "${INSTALL_DIR}/VERSION" ]] && CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
print "The latest version is ${RELEASE_VERSION}, but you have no version installed."
fi
# Check if it's an update or a new install
@@ -275,7 +284,7 @@ if command -v lsb_release &> /dev/null; then
# without writing the AppImage to a non-user-writable location (without invalidating other security
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
HAS_USERNS_RESTRICTIONS=false
if [[ "$DISTVER" =~ ^Ubuntu && $DISTMAJOR -ge 23 ]]; then
if [[ "$DISTVER" =~ ^(Ubuntu|Tuxedo) && $DISTMAJOR -ge 23 ]]; then
HAS_USERNS_RESTRICTIONS=true
fi

View File

@@ -9,7 +9,7 @@
"vips.dev": {
"platforms": ["aarch64-darwin"],
},
"nodejs": "24.4.1",
"nodejs": "24.5.0",
"pkg-config": "latest",
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix

View File

@@ -81,7 +81,7 @@
"eslint-plugin-promise": "6.6.0",
"eslint-plugin-react": "7.37.5",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"fs-extra": "11.3.2",
"glob": "11.0.3",
"gulp": "4.0.2",
"husky": "9.1.7",
@@ -95,7 +95,7 @@
"@types/fs-extra": "11.0.4",
"eslint-plugin-github": "4.10.2",
"http-server": "14.1.1",
"node-gyp": "11.3.0",
"node-gyp": "11.4.2",
"nodemon": "3.1.10"
},
"packageManager": "yarn@4.9.2",

View File

@@ -107,6 +107,7 @@ class Command extends BaseCommand {
userContentBaseUrl: () => joplinServerAuth.userContentBaseUrl,
username: () => joplinServerAuth.email,
password: () => joplinServerAuth.password,
apiKey: () => '',
session: (): Session => null,
});

View File

@@ -2,6 +2,7 @@ import app from '../app';
import Folder from '@joplin/lib/models/Folder';
import BaseCommand from '../base-command';
import setupCommand from '../setupCommand';
import Setting from '@joplin/lib/models/Setting';
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
export const setupCommandForTesting = (CommandClass: any, stdout: Function = null): BaseCommand => {
@@ -18,4 +19,9 @@ export const setupApplication = async () => {
// Some tests also need access to the Redux store
app().initRedux();
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL event, which means state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
Setting.dispatchUpdateAll();
};

View File

@@ -48,7 +48,7 @@
"chalk": "4.1.2",
"compare-version": "0.1.2",
"file-type": "16.5.4",
"fs-extra": "11.2.0",
"fs-extra": "11.3.2",
"html-entities": "1.4.0",
"keytar": "7.9.0",
"md5": "2.3.0",
@@ -57,7 +57,7 @@
"proper-lockfile": "4.1.2",
"redux": "4.2.1",
"server-destroy": "1.0.1",
"sharp": "0.34.3",
"sharp": "0.34.4",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",

View File

@@ -0,0 +1,11 @@
<ul>
<li><a href="https://example.com/" title="This
is a test title
testing!
Test...">Test!</a></li>
<li><a href="http://example.com" title="
Test
">Another test...</a></li>
</ul>

View File

@@ -0,0 +1,5 @@
- [Test!](https://example.com/ "This
is a test title
testing!
Test...")
- [Another test...](http://example.com "Test")

View File

@@ -0,0 +1,74 @@
<!-- From https://en.wikipedia.org/wiki/Collatz_conjecture -->
<math display="block" xmlns="http://www.w3.org/1998/Math/MathML" alttext="{\displaystyle f(n)={\begin{cases}n/2&amp;{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&amp;{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}">
<semantics>
<mrow class="MJX-TeXAtom-ORD">
<mstyle displaystyle="true" scriptlevel="0">
<mi>f</mi>
<mo stretchy="false">(</mo>
<mi>n</mi>
<mo stretchy="false">)</mo>
<mo>=</mo>
<mrow class="MJX-TeXAtom-ORD">
<mrow>
<mo>{</mo>
<mtable columnalign="left left" rowspacing=".2em" columnspacing="1em" displaystyle="false">
<mtr>
<mtd>
<mi>n</mi>
<mrow class="MJX-TeXAtom-ORD">
<mo>/</mo>
</mrow>
<mn>2</mn>
</mtd>
<mtd>
<mrow class="MJX-TeXAtom-ORD">
<mtext>if&nbsp;</mtext>
</mrow>
<mi>n</mi>
<mo>\u2261</mo>
<mn>0</mn>
<mrow class="MJX-TeXAtom-ORD">
<mspace width="0.444em"></mspace>
<mo stretchy="false">(</mo>
<mi>mod</mi>
<mspace width="0.333em"></mspace>
<mn>2</mn>
<mo stretchy="false">)</mo>
</mrow>
<mo>,</mo>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>3</mn>
<mi>n</mi>
<mo>+</mo>
<mn>1</mn>
</mtd>
<mtd>
<mrow class="MJX-TeXAtom-ORD">
<mtext>if&nbsp;</mtext>
</mrow>
<mi>n</mi>
<mo>\u2261</mo>
<mn>1</mn>
<mrow class="MJX-TeXAtom-ORD">
<mspace width="0.444em"></mspace>
<mo stretchy="false">(</mo>
<mi>mod</mi>
<mspace width="0.333em"></mspace>
<mn>2</mn>
<mo stretchy="false">)</mo>
</mrow>
<mo>.</mo>
</mtd>
</mtr>
</mtable>
<mo fence="true" stretchy="true" symmetric="true"></mo>
</mrow>
</mrow>
</mstyle>
</mrow>
<annotation encoding="application/x-tex">{\displaystyle f(n)={\begin{cases}n/2&amp;{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&amp;{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}</annotation>
</semantics>
</math></span><img src="/some/src/here" class="mwe-math-fallback-image-display mw-invert skin-invert" aria-hidden="true" style="vertical-align: -3.171ex; width:45.735ex; height:7.509ex;"/>

View File

@@ -0,0 +1 @@
${\displaystyle f(n)={\begin{cases}n/2&{\text{if }}n\equiv 0{\pmod {2}},\\3n+1&{\text{if }}n\equiv 1{\pmod {2}}.\end{cases}}}$

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# test for joplin import
[https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm\_source%3DYouTube%2520Instagram%26utm\_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm\_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm\_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm\_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm\_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o\_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&\_\_tn\_\_=H-y-R&c[0]=AT0eE6OXx\_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887\_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG\_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo76663333hhsgsu](<https://l.facebook.com/l.php?u=https%3A%2F%2Fix.sk%2FNiBZH%3Futm_source%3DYouTube%2520Instagram%26utm_medium%3D2HIqFSGVVB2mFsVTJClrQ7ZnuGJaUt6hu1MNH0vUMjcrgWnUsK%26utm_campaign%3D%25F0%259F%2598%25A9%25F0%259F%2598%258E%25F0%259F%2598%25BF%25F0%259F%25A4%2594%25F0%259F%2598%25A9%25F0%259F%2599%2583%25F0%259F%25A4%25AF%25F0%259F%25A5%25B0%25F0%259F%2598%25AB%25F0%259F%2598%25BA%26utm_id%3D%25F0%259F%2598%258B%25F0%259F%2598%25A5%25F0%259F%25A4%25A1%25F0%259F%2598%25A0%25F0%259F%2598%2587%25F0%259F%25A5%25B4%25F0%259F%25A7%2590%25F0%259F%2598%258E%25F0%259F%2598%2582%25F0%259F%2598%259E%26utm_term%3D%25F0%259F%2598%2584%25F0%259F%25A4%25A9%25F0%259F%2599%2580%25F0%259F%2598%2593%25F0%259F%25A4%25AF%25F0%259F%25A4%25A5%25F0%259F%2591%25BE%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%25A4%25A5%26utm_content%3D%25F0%259F%2591%25BD%25F0%259F%2598%25AB%25F0%259F%2591%25BF%25F0%259F%2598%25BD%25F0%259F%2598%25A9%25F0%259F%2599%2589%26fbclid%3DIwAR0I3l5DBLypLaTjDTCGPQ1i1MmPB2-pE8iqrxrgUK9Kkvq3OX5Mjejibzw&h=AT3nNxW4G-9nAkhXU1EVN-aVGl1o_-DzDAaWFx9xbprpN3JRBOh17lCQQHNAlIMv6iE4P2vobBAAivLvdzy00K8xqIqb-CvGj6YnnBX6R9wwtj5Y&__tn__=H-y-R&c[0]=AT0eE6OXx_t9HzpPmMgTdOWAw2ZRNPRDIHJWf699NZYkYzugbWS6g3rOndhPA8fwrCIgk1zn2D1To7phLW9wXkqfgZU1ayT3887_dxrfN-x822Pos0lCjTIhoQcxfBl516pTz1XrRG_MbtPpLzUFAGu4nw5W86UR1EkBCZhustNbgTX4wVReiVSuwAWu7Sp1yiWvUm5JXlo>)

View File

@@ -0,0 +1,9 @@
---
id: 20250821081408
date: 2025-08-21
keywords:
---
# A test file for Joplin importer
Test

View File

@@ -0,0 +1,7 @@
---
title: test
created: 2025-07-22 17:30:44Z
updated: 2025-07-22 17:37:48Z
---
test

View File

@@ -1,7 +1,7 @@
---
title: "Frontmatter test"
created_at: 01-01-2024 01:23 AM
updated_at: 02-01-2024 04:56 AM
updated_at: 01-01-2024 04:56 AM
---
# Frontmatter test

View File

@@ -165,6 +165,10 @@
if (a && a.toLowerCase().indexOf('math/tex') >= 0) isVisible = true;
}
if (nodeName === 'annotation') {
if (node.getAttribute('encoding') === 'application/x-tex') isVisible = true;
}
if (nodeName === 'source' && nodeParentName === 'picture') {
isVisible = false;
}

View File

@@ -281,14 +281,16 @@ class Application extends BaseApplication {
}
// As of Joplin 3.5.7, the ABC rendering is part of the app so we automatically disable the plugin
pluginSettings = {
...pluginSettings,
['org.joplinapp.plugins.AbcSheetMusic']: {
enabled: false,
deleted: false,
hasBeenUpdated: false,
},
};
if (pluginSettings['org.joplinapp.plugins.AbcSheetMusic']) {
pluginSettings = {
...pluginSettings,
['org.joplinapp.plugins.AbcSheetMusic']: {
enabled: false,
deleted: false,
hasBeenUpdated: false,
},
};
}
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {

View File

@@ -13,6 +13,7 @@ import * as openProfileDirectory from './openProfileDirectory';
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as showProfileEditor from './showProfileEditor';
import * as startExternalEditing from './startExternalEditing';
import * as stopExternalEditing from './stopExternalEditing';
import * as switchProfile from './switchProfile';
@@ -38,6 +39,7 @@ const index: any[] = [
openSecondaryAppInstance,
replaceMisspelling,
restoreNoteRevision,
showProfileEditor,
startExternalEditing,
stopExternalEditing,
switchProfile,

View File

@@ -0,0 +1,20 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
export const declaration: CommandDeclaration = {
name: 'showProfileEditor',
label: () => _('Manage profiles'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
context.dispatch({
type: 'NAV_GO',
routeName: 'ProfileEditor',
});
},
enabledCondition: 'hasMultiProfiles',
};
};

View File

@@ -852,7 +852,7 @@ const mapStateToProps = (state: AppState) => {
notesColumns: validateColumns(state.settings['notes.columns']),
showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate,
toast: state.toast,
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && process.arch !== 'arm64',
shouldSwitchToAppleSiliconVersion: shim.isAppleSilicon() && shim.isMac() && process.arch !== 'arm64',
};
};

View File

@@ -117,7 +117,7 @@ const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: an
switchProfileMenuItems.push({ type: 'separator' });
switchProfileMenuItems.push(menuItemDic.addProfile);
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
switchProfileMenuItems.push(menuItemDic.showProfileEditor);
return switchProfileMenuItems;
}, [profileConfig, menuItemDic]);

View File

@@ -31,13 +31,15 @@ function markupToHtml() {
}
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, lineSetter: Function) {
function countElements(text: string, wordSetter: Function, characterSetter: Function, characterNoSpaceSetter: Function, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: Function) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
Countable.count(text, (counter: any) => {
wordSetter(counter.words);
characterSetter(counter.all);
characterNoSpaceSetter(counter.characters);
});
const cjkMatches = text.match(/[\p{Script=Han}\p{Script=Bopomofo}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu);
cjkCharacterSetter(cjkMatches ? cjkMatches.length : 0);
lineSetter(text === '' ? 0 : text.split('\n').length);
}
@@ -58,23 +60,25 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
const [words, setWords] = useState<number>(0);
const [characters, setCharacters] = useState<number>(0);
const [charactersNoSpace, setCharactersNoSpace] = useState<number>(0);
const [cjkCharacters, setCjkCharacters] = useState<number>(0);
// For source with Markdown syntax stripped out
const [strippedLines, setStrippedLines] = useState<number>(0);
const [strippedWords, setStrippedWords] = useState<number>(0);
const [strippedCharacters, setStrippedCharacters] = useState<number>(0);
const [strippedCharactersNoSpace, setStrippedCharactersNoSpace] = useState<number>(0);
const [strippedCjkCharacters, setStrippedCjkCharacters] = useState<number>(0);
const [strippedReadTime, setStrippedReadTime] = useState<number>(0);
// This amount based on the following paper:
// https://www.researchgate.net/publication/332380784_How_many_words_do_we_read_per_minute_A_review_and_meta-analysis_of_reading_rate
const wordsPerMinute = 250;
useEffect(() => {
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setLines);
countElements(props.text, setWords, setCharacters, setCharactersNoSpace, setCjkCharacters, setLines);
}, [props.text]);
useEffect(() => {
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedCjkCharacters, setStrippedLines);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.text]);
@@ -88,6 +92,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
words: words,
characters: characters,
charactersNoSpace: charactersNoSpace,
cjkCharacters: cjkCharacters,
};
const strippedTextProperties: TextPropertiesMap = {
@@ -99,12 +104,14 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
words: strippedWords,
characters: strippedCharacters,
charactersNoSpace: strippedCharactersNoSpace,
cjkCharacters: strippedCjkCharacters,
};
const keyToLabel: KeyToLabelMap = {
words: _('Words'),
characters: _('Characters'),
charactersNoSpace: _('Characters excluding spaces'),
cjkCharacters: _('Chinese/Japanese/Korean characters'),
lines: _('Lines'),
};
@@ -147,6 +154,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
);
for (const key in textProperties) {
if (key === 'cjkCharacters' && textProperties[key] === 0 && strippedTextProperties[key] === 0) continue;
const comp = createTableBodyRow(key, textProperties[key], strippedTextProperties[key]);
tableBodyComps.push(comp);
}

View File

@@ -366,6 +366,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
ignoreModifiers: true,
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
keymap: keyboardMode,
preferMacShortcuts: shim.isMac(),
indentWithTabs: true,
tabMovesFocus: props.tabMovesFocus,
editorLabel: _('Markdown editor'),

View File

@@ -1051,6 +1051,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
editor,
});
const noteChangeTimeRef = useRef(Date.now());
const lastNoteIdRef = useRef(props.noteId);
useEffect(() => {
if (!editor) return () => {};
@@ -1068,6 +1069,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// Use nextOnChangeEventInfo's noteId -- lastOnChangeEventInfo can be slightly out-of-date.
const differentNoteId = lastNoteIdRef.current !== props.noteId;
const differentContent = lastOnChangeEventInfo.current.content !== props.content;
if (differentNoteId) noteChangeTimeRef.current = Date.now();
if (differentNoteId || differentContent || !resourcesEqual) {
const result = await props.markupToHtml(
props.contentMarkupLanguage,
@@ -1340,7 +1344,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
// keep it this way for now.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function onKeyUp(event: any) {
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key)) {
const timeSinceNoteChange = Date.now() - noteChangeTimeRef.current;
// A key that is pressed before the editor is opened, and that is released after it is
// opened is going to be processed here. For example if the user presses Enter in
// GotoAnything to arrive here. But in that case, we don't want the change handler to be
// activated, because that would change the note timestamp. So we take into account how
// long the note has been loaded before we process the key. Fixes
// https://github.com/laurent22/joplin/issues/12367
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key) && timeSinceNoteChange > 200) {
onChangeHandler();
}
}

View File

@@ -50,7 +50,7 @@ import WarningBanner from './WarningBanner/WarningBanner';
import UserWebview from '../../services/plugins/UserWebview';
import Logger from '@joplin/utils/Logger';
import usePluginEditorView from './utils/usePluginEditorView';
import { stateUtils } from '@joplin/lib/reducer';
import { defaultWindowId, stateUtils } from '@joplin/lib/reducer';
import { WindowIdContext } from '../NewWindowOrIFrame';
import useResourceUnwatcher from './utils/useResourceUnwatcher';
import StatusBar from './StatusBar';
@@ -722,6 +722,8 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
bodyEditor = 'CodeMirror5';
}
const mainWindowState = stateUtils.windowStateById(state, defaultWindowId);
return {
noteId,
bodyEditor,
@@ -740,7 +742,9 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
customCss: state.customViewerCss,
noteVisiblePanes: windowState.noteVisiblePanes,
watchedResources: windowState.watchedResources,
highlightedWords: state.highlightedWords,
// For now, only the main window has search UI. Show the same search markers in all
// windows:
highlightedWords: mainWindowState.highlightedWords,
plugins: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([

View File

@@ -0,0 +1,47 @@
.profile-management {
font-family: var(--joplin-font-family);
display: flex;
flex-direction: column;
> .tableContainer {
overflow-y: scroll;
padding: 20px;
box-sizing: border-box;
flex: 1 1 0%;
color: var(--joplin-color);
> .notification {
margin-bottom: 10px;
}
}
}
.profile-table {
width: 100%;
> thead > tr > .headerCell {
white-space: nowrap;
font-weight: bold;
width: 1px;
}
> tbody > tr {
> .nameCell {
text-overflow: ellipsis;
overflow-x: hidden;
max-width: 1px;
width: 100%;
white-space: nowrap;
}
> .dataCell {
white-space: nowrap;
width: 1px;
color: var(--joplin-color-faded);
}
> .profileActions > button {
margin-right: 10px;
}
}
}

View File

@@ -0,0 +1,193 @@
import * as React from 'react';
import { useState, useEffect, CSSProperties } from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
import { connect } from 'react-redux';
import { themeStyle } from '@joplin/lib/theme';
import bridge from '../services/bridge';
import dialogs from './dialogs';
import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import { deleteProfileById, saveProfileConfig } from '@joplin/lib/services/profileConfig';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import { AppState } from '../app.reducer';
import { Dispatch } from 'redux';
const logger = Logger.create('ProfileEditor');
interface Props {
themeId: number;
dispatch: Dispatch;
style: CSSProperties;
profileConfig: ProfileConfig;
}
interface ProfileTableProps {
profiles: Profile[];
currentProfileId: string;
onProfileRename: (profile: Profile)=> void;
onProfileDelete: (profile: Profile)=> void;
themeId: number;
}
const ProfileTableComp: React.FC<ProfileTableProps> = props => {
const theme = themeStyle(props.themeId);
return (
<table className="profile-table">
<thead>
<tr>
<th className="headerCell">{_('Profile name')}</th>
<th className="headerCell">{_('ID')}</th>
<th className="headerCell">{_('Status')}</th>
<th className="headerCell">{_('Actions')}</th>
</tr>
</thead>
<tbody>
{props.profiles.map((profile: Profile, index: number) => {
const isCurrentProfile = profile.id === props.currentProfileId;
return (
<tr key={index}>
<td id={`name-${profile.id}`} className="nameCell">
<span style={{ fontWeight: isCurrentProfile ? 'bold' : 'normal' }}>
{profile.name || `(${_('Untitled')})`}
</span>
</td>
<td className="dataCell">{profile.id}</td>
<td className="dataCell">
{isCurrentProfile ? _('Active') : ''}
</td>
<td className="dataCell profileActions">
<button
id={`rename-${profile.id}`}
aria-labelledby={`rename-${profile.id} name-${profile.id}`}
style={theme.buttonStyle}
onClick={() => props.onProfileRename(profile)}
>
{_('Rename')}
</button>
{!isCurrentProfile && (
<button
id={`delete-${profile.id}`}
aria-labelledby={`delete-${profile.id} name-${profile.id}`}
style={theme.buttonStyle}
onClick={() => props.onProfileDelete(profile)}
>
{_('Delete')}
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
};
const ProfileEditorComponent: React.FC<Props> = props => {
const { profileConfig, themeId, dispatch } = props;
const theme = themeStyle(themeId);
const style = props.style;
const containerHeight = style.height;
const [profiles, setProfiles] = useState<Profile[]>(profileConfig.profiles);
useEffect(() => {
setProfiles(profileConfig.profiles);
}, [profileConfig]);
const saveNewProfileConfig = async (makeNewProfileConfig: ()=> ProfileConfig) => {
try {
const newProfileConfig = makeNewProfileConfig();
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newProfileConfig);
dispatch({
type: 'PROFILE_CONFIG_SET',
value: newProfileConfig,
});
} catch (error) {
logger.error(error);
bridge().showErrorMessageBox(error.message);
}
};
const onProfileRename = async (profile: Profile) => {
const newName = await dialogs.prompt(_('Profile name:'), '', profile.name);
if (newName === null || newName === undefined || newName === profile.name) return;
if (!newName.trim()) {
bridge().showErrorMessageBox(_('Profile name cannot be empty'));
return;
}
const makeNewProfileConfig = () => {
const newProfiles = profileConfig.profiles.map(p =>
p.id === profile.id ? { ...p, name: newName.trim() } : p,
);
const newProfileConfig = {
...profileConfig,
profiles: newProfiles,
};
return newProfileConfig;
};
await saveNewProfileConfig(makeNewProfileConfig);
};
const onProfileDelete = async (profile: Profile) => {
const isCurrentProfile = profile.id === profileConfig.currentProfileId;
if (isCurrentProfile) {
bridge().showErrorMessageBox(_('The active profile cannot be deleted. Switch to a different profile and try again.'));
return;
}
const ok = bridge().showConfirmMessageBox(_('Delete profile "%s"?\n\nAll data, including notes, notebooks and tags will be permanently deleted.', profile.name), {
buttons: [_('Delete'), _('Cancel')],
defaultId: 1,
});
if (!ok) return;
const rootDir = Setting.value('rootProfileDir');
const profileDir = `${rootDir}/profile-${profile.id}`;
try {
await shim.fsDriver().remove(profileDir);
logger.info('Deleted profile directory: ', profileDir);
} catch (error) {
logger.error('Error deleting profile directory: ', error);
bridge().showErrorMessageBox(error.message);
}
await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id));
};
return (
<div className="profile-management" style={{ ...theme.containerStyle, height: containerHeight }}>
<div className="tableContainer">
<div className="notification" style={theme.notificationBox}>
{_('Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.')}
</div>
<ProfileTableComp
themeId={themeId}
profiles={profiles}
currentProfileId={profileConfig.currentProfileId}
onProfileRename={onProfileRename}
onProfileDelete={onProfileDelete}
/>
</div>
<ButtonBar
onCancelClick={() => dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
};
const mapStateToProps = (state: AppState) => ({
themeId: state.settings.theme,
profileConfig: state.profileConfig,
});
export default connect(mapStateToProps)(ProfileEditorComponent);

View File

@@ -84,7 +84,13 @@ const ResourceTableComp = (props: ResourceTable) => {
};
const filteredResources = props.resources.filter(
(resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter),
(resource: InnerResource) => {
if (props.filter) {
const filterLowerCase = props.filter.toLowerCase();
return resource.title?.toLowerCase().includes(filterLowerCase) || resource.id.toLowerCase().includes(filterLowerCase);
}
return true;
},
);
const renderSortableHeader = (title: string, order: SortingOrder) => {
@@ -297,7 +303,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
<div style={{ ...theme.notificationBox, marginBottom: 10 }}>{
_('This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.')
}</div>
<div style={{ float: 'right' }}>
<p style={{ float: 'left', paddingRight: 10 }}>
<input
style={theme.inputStyle}
type="search"
@@ -305,7 +311,7 @@ class ResourceScreenComponent extends React.Component<Props, State> {
onChange={this.onFilterUpdate}
placeholder={_('Search...')}
/>
</div>
</p>
{this.state.isLoading && <div>{_('Please wait...')}</div>}
{!this.state.isLoading && <div>
{!this.state.resources && <div>

View File

@@ -20,6 +20,7 @@ import Dialog from './Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
import ImportScreen from './ImportScreen';
import ResourceScreen from './ResourceScreen';
import ProfileEditor from './ProfileEditor';
import Navigator from './Navigator';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
@@ -165,6 +166,7 @@ class RootComponent extends React.Component<Props, any> {
Import: { screen: ImportScreen, title: () => _('Import') },
Config: { screen: ConfigScreen, title: () => _('Options') },
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
ProfileEditor: { screen: ProfileEditor, title: () => _('Manage profiles') },
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
};

View File

@@ -3,7 +3,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
import { _, _n } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
import DialogButtonRow from './DialogButtonRow';
import { themeStyle, buildStyle } from '@joplin/lib/theme';
import Dialog from './Dialog';
import DialogTitle from './DialogTitle';
import ShareService from '@joplin/lib/services/share/ShareService';
@@ -29,47 +28,6 @@ interface Props {
syncTargetId: number;
}
function styles_(props: Props) {
return buildStyle('ShareNoteDialog', props.themeId, theme => {
return {
root: {
minWidth: 500,
},
noteList: {
marginBottom: 10,
},
note: {
flex: 1,
flexDirection: 'row',
display: 'flex',
alignItems: 'center',
border: '1px solid',
borderColor: theme.dividerColor,
padding: '0.5em',
marginBottom: 5,
},
noteTitle: {
...theme.textStyle,
flex: 1,
display: 'flex',
color: theme.color,
},
noteRemoveButton: {
background: 'none',
border: 'none',
},
noteRemoveButtonIcon: {
color: theme.color,
fontSize: '1.4em',
},
copyShareLinkButton: {
...theme.buttonStyle,
marginBottom: 10,
},
};
});
}
export function ShareNoteDialog(props: Props) {
const [notes, setNotes] = useState<NoteEntity[]>([]);
const [recursiveShare, setRecursiveShare] = useState<boolean>(false);
@@ -77,8 +35,6 @@ export function ShareNoteDialog(props: Props) {
const syncTargetInfo = useMemo(() => SyncTargetRegistry.infoById(props.syncTargetId), [props.syncTargetId]);
const noteCount = notes.length;
const theme = themeStyle(props.themeId);
const styles = styles_(props);
useEffect(() => {
void ShareService.instance().refreshShares();
@@ -117,8 +73,8 @@ export function ShareNoteDialog(props: Props) {
);
return (
<div key={note.id} style={styles.note}>
<span style={styles.noteTitle}>{note.title}</span>{unshareButton}
<div key={note.id} className='shared-note-list-item'>
<span className='title'>{note.title}</span>{unshareButton}
</div>
);
};
@@ -128,7 +84,7 @@ export function ShareNoteDialog(props: Props) {
for (const note of notes) {
noteComps.push(renderNote(note));
}
return <div style={styles.noteList}>{noteComps}</div>;
return <div className="notes">{noteComps}</div>;
};
const statusMessage = useShareStatusMessage({ sharesState, noteCount });
@@ -136,7 +92,7 @@ export function ShareNoteDialog(props: Props) {
function renderEncryptionWarningMessage() {
if (!encryptionWarning) return null;
return <div style={theme.textStyle}>{encryptionWarning}<hr/></div>;
return <div className="message">{encryptionWarning}<hr/></div>;
}
const onRecursiveShareChange = useCallback(() => {
@@ -155,12 +111,16 @@ export function ShareNoteDialog(props: Props) {
const renderContent = () => {
return (
<div style={styles.root} className="form">
<div className="form share-note-dialog">
<DialogTitle title={_('Publish Notes')}/>
{renderNoteList(notes)}
{renderRecursiveShareCheckbox()}
<button disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage}</div>
<button
disabled={[SharingStatus.Creating, SharingStatus.Synchronizing].indexOf(sharesState) >= 0}
className="share"
onClick={shareLinkButton_click}
>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div className="message">{statusMessage}</div>
{renderEncryptionWarningMessage()}
<DialogButtonRow
themeId={props.themeId}

View File

@@ -10,8 +10,11 @@ import Setting from '@joplin/lib/models/Setting';
import { PackageInfo } from '@joplin/lib/versionInfo';
import shim from '@joplin/lib/shim';
import { ImportModule } from '@joplin/lib/services/interop/Module';
import Logger from '@joplin/utils/Logger';
const packageInfo: PackageInfo = require('../../../packageInfo.js');
const logger = Logger.create('importFrom');
export const declaration: CommandDeclaration = {
name: 'importFrom',
label: () => _('Import...'),
@@ -135,6 +138,7 @@ export const runtime = (control: WindowControl): CommandRuntime => {
// eslint-disable-next-line no-console
console.info('Import result: ', result);
} catch (error) {
logger.error(error);
bridge().showErrorMessageBox(error.message);
}

View File

@@ -73,7 +73,7 @@ export default function() {
'commandPalette',
'openMasterPasswordDialog',
'addProfile',
'editProfileConfig',
'showProfileEditor',
'switchProfile1',
'switchProfile2',
'switchProfile3',

View File

@@ -0,0 +1,16 @@
// Corresponds to theme.buttonStyle
.base-button {
border: 1px solid;
min-height: 26px;
min-width: 80px;
padding-left: 12px;
padding-right: 12px;
padding-top: 6px;
padding-bottom: 6px;
border-radius: 4px;
color: var(--joplin-color4);
background-color: var(--joplin-background-color4);
border-color: var(--joplin-border-color4);
user-select: none;
}

View File

@@ -0,0 +1,8 @@
// Corresponds to theme.textStyle
.base-text {
font-family: var(--joplin-font-family);
font-size: var(--joplin-font-size);
line-height: 1.6em;
color: var(--joplin-color);
}

View File

@@ -1,4 +1,6 @@
@use './base-text.scss';
@use './base-button.scss';
@use './dialog-modal-layer.scss';
@use './user-webview-dialog.scss';
@use './prompt-dialog.scss';
@@ -19,3 +21,5 @@
@use './popup-notification-list.scss';
@use './popup-notification-item.scss';
@use './multi-note-actions.scss';
@use './share-note-dialog.scss';
@use './shared-note-list-item.scss';

View File

@@ -0,0 +1,19 @@
@use "./base-button.scss";
@use "./base-text.scss";
.share-note-dialog {
min-width: 500px;
> .message {
@extend .base-text;
}
> .notes {
margin-bottom: 10px;
}
> .share {
@extend .base-button;
margin-bottom: 10px;
}
}

View File

@@ -0,0 +1,20 @@
@use "./base-text.scss";
.shared-note-list-item {
flex: 1;
flex-direction: row;
display: flex;
align-items: center;
border: 1px solid;
border-color: var(--joplin-divider-color);
padding: 0.5em;
margin-bottom: 5px;
> .title {
@extend .base-text;
flex: 1;
display: flex;
color: var(--joplin-color);
}
}

View File

@@ -145,7 +145,7 @@
"@joplin/renderer": "~3.5",
"@joplin/tools": "~3.5",
"@joplin/utils": "~3.5",
"@playwright/test": "1.54.2",
"@playwright/test": "1.55.0",
"@sentry/electron": "4.24.0",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.14",
@@ -209,7 +209,7 @@
"dependencies": {
"@electron/remote": "2.1.3",
"@joplin/onenote-converter": "~3.5",
"fs-extra": "11.2.0",
"fs-extra": "11.3.2",
"keytar": "7.9.0",
"node-fetch": "2.6.7",
"sqlite3": "5.1.6"

View File

@@ -29,12 +29,9 @@ export interface Props {
borderBottom?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
theme?: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onSubmit?: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onDismiss?: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onReady?: Function;
onSubmit?: ()=> void;
onDismiss?: ()=> void;
onReady?: ()=> void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@@ -1,13 +1,14 @@
import { RefObject } from 'react';
import useMessageHandler from './useMessageHandler';
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
export default function(viewRef: RefObject<HTMLIFrameElement>, onSubmit: Function, onDismiss: Function) {
type OnEvent = ()=> void;
export default function(viewRef: RefObject<HTMLIFrameElement>, onSubmit: OnEvent, onDismiss: OnEvent) {
useMessageHandler(viewRef, event => {
const message = event.data?.message;
if (message === 'form-submit') {
if (message === 'form-submit' && onSubmit) {
onSubmit();
} else if (message === 'dismiss') {
} else if (message === 'dismiss' && onDismiss) {
onDismiss();
}
});

View File

@@ -13,6 +13,7 @@
@use 'gui/Sidebar/style.scss' as sidebar-styles;
@use 'gui/NoteEditor/style.scss' as note-editor-styles;
@use 'gui/KeymapConfig/style.scss' as keymap-styles;
@use 'gui/ProfileEditor.scss' as profile-editor;
@use 'services/plugins/styles/index.scss' as plugins-styles;
@use 'gui/styles/index.scss' as gui-styles;
@use 'main.scss' as main;

View File

@@ -89,8 +89,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097781
versionName "3.5.1"
versionCode 2097783
versionName "3.5.3"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
@@ -145,5 +145,3 @@ dependencies {
implementation jscFlavor
}
}
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -36,8 +36,5 @@ rootProject.name = 'Joplin'
expoAutolinking.useExpoVersionCatalog()
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':app'
includeBuild(expoAutolinking.reactNativeGradlePlugin)

View File

@@ -29,7 +29,7 @@ const useStyles = () => {
const TextInputDialog: React.FC<Props> = ({ dialog, containerStyle, themeId }) => {
const styles = useStyles();
const [text, setText] = useState('');
const [text, setText] = useState(dialog.initialValue ?? '');
const labelId = useId();
return (

View File

@@ -91,7 +91,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
});
});
},
promptForText: (message: string) => {
promptForText: (message: string, initialValue?: string) => {
return new Promise<string|null>((resolve) => {
const dismiss = () => {
onDismiss(dialog);
@@ -101,6 +101,7 @@ const useDialogControl = (setPromptDialogs: SetPromptDialogs) => {
type: DialogType.TextInput,
key: `prompt-dialog-${nextDialogIdRef.current++}`,
message,
initialValue,
onSubmit: (text) => {
resolve(text);
dismiss();

View File

@@ -24,7 +24,7 @@ export interface DialogControl {
info(message: string): Promise<void>;
error(message: string): Promise<void>;
prompt(title: string, message: string, buttons?: PromptButtonSpec[], options?: PromptOptions): void;
promptForText(message: string): Promise<string>;
promptForText(message: string, initialValue?: string): Promise<string>;
showMenu<IdType>(title: string, choices: MenuChoice<IdType>[]): Promise<IdType>;
}
@@ -47,6 +47,7 @@ export interface TextInputDialogData {
type: DialogType.TextInput;
key: string;
message: string;
initialValue?: string;
onSubmit: (text: string)=> void;
onDismiss: ()=> void;
}

View File

@@ -2,10 +2,9 @@
import * as React from 'react';
import { TextStyle, Text, StyleProp } from 'react-native';
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
const AntIcon = require('react-native-vector-icons/AntDesign').default;
const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default;
const Ionicon = require('react-native-vector-icons/Ionicons').default;
import { FontAwesome5 } from '@react-native-vector-icons/fontawesome5';
import { MaterialDesignIcons } from '@react-native-vector-icons/material-design-icons';
import { Ionicons } from '@react-native-vector-icons/ionicons';
interface Props {
name: string;
@@ -43,20 +42,24 @@ const Icon: React.FC<Props> = props => {
};
if (namePrefix.match(/^fa[bsr]?$/)) {
let iconStyle = 'solid';
if (namePrefix.startsWith('fab')) {
iconStyle = 'brand';
} else if (namePrefix.startsWith('fas')) {
iconStyle = 'solid';
}
return (
<FontAwesomeIcon
brand={namePrefix.startsWith('fab')}
solid={namePrefix.startsWith('fas')}
<FontAwesome5
name={nameSuffix}
iconStyle={iconStyle}
{...sharedProps}
/>
);
} else if (namePrefix === 'ant') {
return <AntIcon name={nameSuffix} {...sharedProps}/>;
} else if (namePrefix === 'material') {
return <MaterialCommunityIcon name={nameSuffix} {...sharedProps}/>;
return <MaterialDesignIcons name={nameSuffix} {...sharedProps}/>;
} else if (namePrefix === 'ionicon') {
return <Ionicon name={nameSuffix} {...sharedProps}/>;
return <Ionicons name={nameSuffix} {...sharedProps}/>;
} else if (namePrefix === 'text') {
return (
<Text
@@ -69,7 +72,7 @@ const Icon: React.FC<Props> = props => {
</Text>
);
} else {
return <FontAwesomeIcon name='cog' {...sharedProps}/>;
return <FontAwesome5 name='cog' {...sharedProps}/>;
}
};

View File

@@ -274,6 +274,7 @@ function NoteEditor(props: Props) {
highlightActiveLine,
keymap: EditorKeymap.Default,
preferMacShortcuts: shim.mobilePlatform() === 'ios',
automatchBraces: false,
ignoreModifiers: false,

View File

@@ -215,8 +215,7 @@ describe('RichTextEditor', () => {
firstCheckbox.click();
await waitFor(async () => {
// At present, lists are saved as non-tight lists:
expect(body.trim()).toBe('- [x] Test\n \n- [x] Another test');
expect(body.trim()).toBe('- [x] Test\n- [x] Another test');
});
});
@@ -433,8 +432,14 @@ describe('RichTextEditor', () => {
expect(editor.textContent).toContain('3^2 + 4^2 = 5^2');
});
it('should save lists as single-spaced', async () => {
let body = 'Test:\n\n- this\n- is\n- a\n- test.';
it.each(['-', '- [ ]'])('should save lists as single-spaced (list markers: %j)', async (marker) => {
let body = [
'Test:\n',
'this',
'is',
'a',
'test.',
].join(`\n${marker} `);
render(<WrappedEditor
noteBody={body}
@@ -445,7 +450,13 @@ describe('RichTextEditor', () => {
mockTyping(window, ' Testing');
await waitFor(async () => {
expect(body.trim()).toBe('Test:\n\n- this\n- is\n- a\n- test. Testing');
expect(body.trim()).toBe([
'Test:\n',
'this',
'is',
'a',
'test. Testing',
].join(`\n${marker} `));
});
});

View File

@@ -126,12 +126,12 @@ const declarations: CommandDeclaration[] = [
{
name: EditorCommandType.IndentLess,
label: () => _('Decrease indent level'),
iconName: 'ant indent-left',
iconName: 'material format-indent-decrease',
},
{
name: EditorCommandType.IndentMore,
label: () => _('Increase indent level'),
iconName: 'ant indent-right',
iconName: 'material format-indent-increase',
},
{
name: `editor.${EditorCommandType.SwapLineDown}`,

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { PureComponent, ReactElement } from 'react';
import { connect } from 'react-redux';
import { View, Text, StyleSheet, TouchableOpacity, ViewStyle, Platform } from 'react-native';
const Icon = require('react-native-vector-icons/Ionicons').default;
import BackButtonService from '../../services/BackButtonService';
import NavService from '@joplin/lib/services/NavService';
import { _, _n } from '@joplin/lib/locale';
@@ -26,6 +25,7 @@ import WebBetaButton from './WebBetaButton';
import Menu, { MenuOptionType } from './Menu';
import shim from '@joplin/lib/shim';
import CommandService from '@joplin/lib/services/CommandService';
import Icon from '../Icon';
export { MenuOptionType };
// Rather than applying a padding to the whole bar, it is applied to each
@@ -282,7 +282,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
accessibilityHint={_('Show/hide the sidebar')}
accessibilityRole="button">
<View style={styles.sideMenuButton}>
<Icon name="menu" style={styles.topIcon} />
<Icon name="ionicon menu" style={styles.topIcon} accessibilityLabel={null} />
</View>
</TouchableOpacity>
);
@@ -299,8 +299,9 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
accessibilityRole="button">
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
<Icon
name="arrow-back"
name="ionicon arrow-back"
style={styles.topIcon}
accessibilityLabel={null}
/>
</View>
</TouchableOpacity>
@@ -688,7 +689,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
!menuOptions.length || !showContextMenuButton ? null : (
<Menu themeId={this.props.themeId} options={menuOptions}>
<View style={contextMenuStyle} accessibilityLabel={_('Actions')}>
<Icon name="ellipsis-vertical" style={this.styles().contextMenuTrigger} />
<Icon name="ionicon ellipsis-vertical" style={this.styles().contextMenuTrigger} accessibilityLabel={null}/>
</View>
</Menu>
);

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import { useMemo } from 'react';
import { TouchableOpacity, Text, StyleSheet, ScrollView, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
const Icon = require('react-native-vector-icons/Ionicons').default;
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
import { themeStyle } from './global-style';
import { AppState } from '../utils/types';

View File

@@ -11,7 +11,7 @@ export enum InstallState {
interface Props {
onPress: ()=> void;
disabled: boolean;
disabled?: boolean;
children: React.ReactNode;
style?: ViewStyle;
testID?: string;

View File

@@ -6,7 +6,7 @@ import { Dispatch } from 'redux';
import { AccessibilityActionEvent, AccessibilityActionInfo, View } from 'react-native';
import { connect } from 'react-redux';
import BottomDrawer from '../BottomDrawer';
const Icon = require('react-native-vector-icons/Ionicons').default;
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
type OnButtonPress = ()=> void;
interface ButtonSpec {

View File

@@ -632,6 +632,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
if (Platform.OS !== 'ios') addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/');
addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/');
addSettingLink('license_link', _('Open-source licences'), 'https://raw.githubusercontent.com/laurent22/joplin/refs/heads/dev/readme/licenses.md');
const versionInfoText = getVersionInfoText(settings['plugins.states']);

View File

@@ -70,7 +70,6 @@ const PluginBox: React.FC<Props> = props => {
style={styles.cardContainer}
onPress={props.onShowPluginInfo ? onPress : null}
testID='plugin-card'
disabled={!props.isCompatible}
>
<Card.Content style={styles.content}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>

View File

@@ -11,9 +11,9 @@ import { Button } from 'react-native-paper';
import createRootStyle from '../../utils/createRootStyle';
import ScreenHeader from '../ScreenHeader';
import Clipboard from '@react-native-clipboard/clipboard';
const Icon = require('react-native-vector-icons/Ionicons').default;
import Logger from '@joplin/utils/Logger';
import { reg } from '@joplin/lib/registry';
import Icon from '../Icon';
const logger = Logger.create('JoplinCloudLoginScreen');
@@ -179,7 +179,7 @@ const JoplinCloudScreenComponent = (props: Props) => {
</Text>
{state.active === 'LINK_USED' ? (
<Animated.View style={{ transform: [{ rotate: syncIconRotation }] }}>
<Icon name='sync' style={styles.loadingIcon}/>
<Icon name='ionicon sync' style={styles.loadingIcon} accessibilityLabel={_('Waiting for authorisation...')}/>
</Animated.View>
) : null }
</View>

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
import { View, Text, FlatList, StyleSheet, AccessibilityRole } from 'react-native';
import { connect } from 'react-redux';
import Tag from '@joplin/lib/models/Tag';
import { themeStyle } from '../global-style';
@@ -8,10 +8,15 @@ import { ScreenHeader } from '../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import { AppState } from '../../utils/types';
import { TagEntity } from '@joplin/lib/services/database/types';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useState, useContext } from 'react';
import { Dispatch } from 'redux';
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getCollator';
import { DialogContext } from '../DialogManager';
import useOnLongPressProps from '../../utils/hooks/useOnLongPressProps';
import { substrWithEllipsis } from '@joplin/lib/string-utils';
import { PromptButtonSpec } from '../DialogManager/types';
import MultiTouchableOpacity from '../buttons/MultiTouchableOpacity';
import SearchBar from './SearchScreen/SearchBar';
import Logger from '@joplin/utils/Logger';
@@ -47,12 +52,47 @@ const useStyles = (themeId: number) => {
}, [themeId]);
};
interface TagItemProps {
tag: TagEntity;
themeId: number;
onPress: (id: string)=> void;
onLongPress: (tag: TagEntity)=> void;
}
const TagItem: React.FC<TagItemProps> = ({ tag, themeId, onPress, onLongPress }) => {
const styles = useStyles(themeId);
const onLongPressProps = useOnLongPressProps({ onLongPress: () => onLongPress(tag), actionDescription: _('Edit tag') });
const accessibilityRole: AccessibilityRole = 'button';
const pressableProps = {
accessibilityRole,
accessibilityHint: _('Shows notes for tag'),
...onLongPressProps,
};
return (
<MultiTouchableOpacity
{...pressableProps}
containerProps={{
style: {},
}}
onPress={() => onPress(tag.id)}
beforePressable={null}
>
<View style={styles.listItem}>
<Text style={styles.listItemText}>{tag.title}</Text>
</View>
</MultiTouchableOpacity>
);
};
const TagsScreenComponent: React.FC<Props> = props => {
const [tags, setTags] = useState<TagEntity[]>([]);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
const [showSearch, setShowSearch] = useState(false);
const styles = useStyles(props.themeId);
const dialogs = useContext(DialogContext);
const collatorLocale = getCollatorLocale();
const collator = useMemo(() => {
return getCollator(collatorLocale);
@@ -86,7 +126,7 @@ const TagsScreenComponent: React.FC<Props> = props => {
setTags([]);
}
}
}, [searchQuery, collator], { interval: 200 });
}, [searchQuery, collator, refreshTrigger], { interval: 200 });
const onSearchButtonPress = useCallback(() => {
setShowSearch(!showSearch);
@@ -111,20 +151,74 @@ const TagsScreenComponent: React.FC<Props> = props => {
});
}, [props.dispatch]);
const onTagItemLongPress = useCallback(async (tag: TagEntity) => {
const menuItems: PromptButtonSpec[] = [];
const generateTagDeletion = () => {
return () => {
dialogs.prompt('', _('Delete tag "%s"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.', substrWithEllipsis(tag.title, 0, 32)), [
{
text: _('OK'),
onPress: async () => {
await Tag.delete(tag.id, { sourceDescription: 'tags-screen (long-press)' });
setRefreshTrigger(prev => prev + 1);
},
},
{
text: _('Cancel'),
onPress: () => { },
style: 'cancel',
},
]);
};
};
menuItems.push({
text: _('Rename'),
onPress: async () => {
const newName = await dialogs.promptForText(_('Rename tag:'), tag.title);
if (newName && newName.trim() && newName.trim() !== tag.title) {
try {
const updatedTag = { ...tag, title: newName };
await Tag.save(updatedTag, { fields: ['title'], userSideValidation: true });
setRefreshTrigger(prev => prev + 1);
} catch (error) {
await dialogs.error(error instanceof Error ? error.message : String(error));
}
}
},
});
menuItems.push({
text: _('Delete'),
onPress: generateTagDeletion(),
style: 'destructive',
});
menuItems.push({
text: _('Cancel'),
onPress: () => {},
style: 'cancel',
});
dialogs.prompt(
'',
_('Tag: %s', tag.title),
menuItems,
);
}, [dialogs]);
type RenderItemEvent = { item: TagEntity };
const onRenderItem = useCallback(({ item }: RenderItemEvent) => {
return (
<TouchableOpacity
onPress={() => onTagItemPress({ id: item.id })}
accessibilityRole='button'
accessibilityHint={_('Shows notes for tag')}
>
<View style={styles.listItem}>
<Text style={styles.listItemText}>{item.title}</Text>
</View>
</TouchableOpacity>
<TagItem
tag={item}
themeId={props.themeId}
onPress={(id) => onTagItemPress({ id })}
onLongPress={onTagItemLongPress}
/>
);
}, [onTagItemPress, styles]);
}, [onTagItemPress, onTagItemLongPress, props.themeId]);
return (
<View style={styles.rootStyle}>

View File

@@ -3,7 +3,6 @@ import { useMemo, useEffect, useCallback, useContext } from 'react';
import { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Image, ImageStyle } from 'react-native';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
const IonIcon = require('react-native-vector-icons/Ionicons').default;
import Icon from './Icon';
import Folder from '@joplin/lib/models/Folder';
import Synchronizer from '@joplin/lib/Synchronizer';
@@ -26,6 +25,7 @@ import { StateDecryptionWorker, StateResourceFetcher } from '@joplin/lib/reducer
import useOnLongPressProps from '../utils/hooks/useOnLongPressProps';
import { TouchableRipple } from 'react-native-paper';
import shim from '@joplin/lib/shim';
import getConflictFolderId from '@joplin/lib/models/utils/getConflictFolderId';
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
interface Props {
@@ -202,8 +202,8 @@ const FolderItem: React.FC<FolderItemProps> = props => {
const baseStyles = props.styles;
const collapsed = props.collapsed;
const iconName = collapsed ? 'chevron-down' : 'chevron-up';
const iconComp = <IonIcon name={iconName} style={baseStyles.folderToggleIcon} />;
const iconName = collapsed ? 'ionicon chevron-down' : 'ionicon chevron-up';
const iconComp = <Icon name={iconName} style={baseStyles.folderToggleIcon} accessibilityLabel={null} />;
const onTogglePress = useCallback(() => {
props.onTogglePress(props.folder);
@@ -232,7 +232,7 @@ const FolderItem: React.FC<FolderItemProps> = props => {
if (folderId === getTrashFolderId()) {
folderIcon = getTrashFolderIcon(FolderIconType.FontAwesome);
} else if (props.alwaysShowFolderIcons) {
return <IonIcon name="folder-outline" style={baseStyles.folderBaseIcon} />;
return <Icon name="ionicon folder-outline" style={baseStyles.folderBaseIcon} accessibilityLabel={null} />;
} else {
return null;
}
@@ -338,6 +338,8 @@ const SideMenuContentComponent = (props: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const menuItems: any[] = [];
if (folder && folder.id === getConflictFolderId()) return;
if (folder && folder.id === getTrashFolderId()) {
menuItems.push({
text: _('Empty trash'),

View File

@@ -2,26 +2,6 @@
* @jest-environment jsdom
*/
import { writeFileSync } from 'fs-extra';
import { join } from 'path';
import Setting from '@joplin/lib/models/Setting';
// Mock react-native-vector-icons -- it uses ESM imports, which, by default, are not
// supported by jest.
jest.doMock('react-native-vector-icons/Ionicons', () => {
return {
default: {
getImageSourceSync: () => {
// Create an empty file that can be read/used as an image resource.
const iconPath = join(Setting.value('cacheDir'), 'test-icon.png');
writeFileSync(iconPath, '', 'utf-8');
return { uri: iconPath };
},
},
};
});
import lightTheme from '@joplin/lib/themes/light';
import { editPopupClass, getEditPopupSource } from './useEditPopup';
import { describe, it, expect, beforeAll, jest } from '@jest/globals';

View File

@@ -6,7 +6,7 @@ import { useMemo } from 'react';
import { extname } from 'path';
import shim from '@joplin/lib/shim';
import { Platform } from 'react-native';
const Icon = require('react-native-vector-icons/Ionicons').default;
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
export const editPopupClass = 'joplin-editPopup';

View File

@@ -342,30 +342,17 @@
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/ionicons/fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/material-design-icons/fonts/MaterialDesignIcons.ttf",
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/material-icons/fonts/MaterialIcons.ttf",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -375,30 +362,17 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialDesignIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;

View File

@@ -42,7 +42,6 @@ target 'Joplin' do
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'JoplinRNShareExtension', :path => 'ShareExtension'
post_install do |installer|

View File

@@ -1516,10 +1516,38 @@ PODS:
- React
- react-native-saf-x (3.5.1):
- React-Core
- react-native-safe-area-context (5.5.2):
- react-native-safe-area-context (5.6.1):
- React-Core
- react-native-sqlite-storage (6.0.1):
- React-Core
- react-native-vector-icons (12.3.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-vector-icons-fontawesome5 (12.3.0)
- react-native-vector-icons-ionicons (12.3.0)
- react-native-vector-icons-material-design-icons (12.4.0)
- react-native-vector-icons-material-icons (12.4.0)
- react-native-version-info (1.1.1):
- React-Core
- react-native-webview (13.15.0):
@@ -1890,7 +1918,7 @@ PODS:
- React
- RNSecureRandom (1.0.1):
- React
- RNShare (12.1.2):
- RNShare (12.2.0):
- DoubleConversion
- glog
- hermes-engine
@@ -1916,30 +1944,6 @@ PODS:
- Yoga
- RNSVG (15.13.0):
- React-Core
- RNVectorIcons (10.3.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-hermes
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- SocketRocket (0.7.1)
- Yoga (0.0.0)
- ZXingObjC/Core (3.6.9)
@@ -2012,6 +2016,11 @@ DEPENDENCIES:
- "react-native-saf-x (from `../node_modules/@joplin/react-native-saf-x`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`)
- "react-native-vector-icons (from `../node_modules/@react-native-vector-icons/get-image`)"
- "react-native-vector-icons-fontawesome5 (from `../node_modules/@react-native-vector-icons/fontawesome5`)"
- "react-native-vector-icons-ionicons (from `../node_modules/@react-native-vector-icons/ionicons`)"
- "react-native-vector-icons-material-design-icons (from `../node_modules/@react-native-vector-icons/material-design-icons`)"
- "react-native-vector-icons-material-icons (from `../node_modules/@react-native-vector-icons/material-icons`)"
- react-native-version-info (from `../node_modules/react-native-version-info`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
@@ -2058,7 +2067,6 @@ DEPENDENCIES:
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
- RNShare (from `../node_modules/react-native-share`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -2191,6 +2199,16 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-sqlite-storage:
:path: "../node_modules/react-native-sqlite-storage"
react-native-vector-icons:
:path: "../node_modules/@react-native-vector-icons/get-image"
react-native-vector-icons-fontawesome5:
:path: "../node_modules/@react-native-vector-icons/fontawesome5"
react-native-vector-icons-ionicons:
:path: "../node_modules/@react-native-vector-icons/ionicons"
react-native-vector-icons-material-design-icons:
:path: "../node_modules/@react-native-vector-icons/material-design-icons"
react-native-vector-icons-material-icons:
:path: "../node_modules/@react-native-vector-icons/material-icons"
react-native-version-info:
:path: "../node_modules/react-native-version-info"
react-native-webview:
@@ -2283,8 +2301,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-share"
RNSVG:
:path: "../node_modules/react-native-svg"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -2349,8 +2365,13 @@ SPEC CHECKSUMS:
react-native-quick-crypto: b475b71e7fa4dbf3446be55e8ad4ef2c58ac4f7f
react-native-rsa-native: a7931cdda1f73a8576a46d7f431378c5550f0c38
react-native-saf-x: 404f0f9a29cc6bf21d88582e054c45a11b28c22b
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
react-native-vector-icons: a45ecc326ec090450f152dfc7076ce1173331ce5
react-native-vector-icons-fontawesome5: 271d813e27a86d30bb8cf1fc2f12dae74b74b69b
react-native-vector-icons-ionicons: ad07e944a092a5cf71b8b569d8f5ce2bf674c415
react-native-vector-icons-material-design-icons: 76cd460b3540b80527b4a80fb7f867f7deedb498
react-native-vector-icons-material-icons: d67e485a05560416ff6b5977d5fa7e0eb6af6870
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
react-native-webview: 0dceb35a9d050f5fa55f7fe2d8c4d1903651eb7d
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
@@ -2395,13 +2416,12 @@ SPEC CHECKSUMS:
RNLocalize: 3c4d0abd777a546fa77bdb6caef85a87fb9ea349
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
RNShare: 6496fc1ea6e8fce76b769513b6c2852f9c3ded82
RNShare: 40ace3f87cd881869e8085aced9dc16b425c74aa
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
RNVectorIcons: e431ef1e6bef75d6ad0e33a83d376e6207962a9d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 8140bf5e9b1f33537d13122a2fecbacaefb2ee5b
PODFILE CHECKSUM: 862189470c6e7bbee6a39c783bf65a36b631921c
COCOAPODS: 1.16.2

View File

@@ -112,14 +112,22 @@ jest.doMock('@expo/vector-icons/MaterialCommunityIcons', () => {
throw new Error('Not supported in testing environments.');
});
// Used by the renderer
jest.doMock('react-native-vector-icons/Ionicons', () => {
return {
default: class extends require('react-native').View {
const mockIconLibrary = (libraryName, exportName) => {
jest.doMock(libraryName, () => {
const MockIconComponent = class extends require('react-native').View {
// Used by the renderer
static getImageSourceSync = () => ({ uri: '' });
},
};
});
};
return {
default: MockIconComponent,
[exportName]: MockIconComponent,
};
});
};
mockIconLibrary('@react-native-vector-icons/ionicons', 'Ionicons');
mockIconLibrary('@react-native-vector-icons/material-design-icons', 'MaterialDesignIcons');
mockIconLibrary('@react-native-vector-icons/fontawesome5', 'FontAwesome5');
// react-native-fs's CachesDirectoryPath export doesn't work in a testing environment.
// Use a temporary folder instead.

View File

@@ -28,12 +28,18 @@
"@joplin/react-native-saf-x": "~3.5",
"@joplin/renderer": "~3.5",
"@joplin/utils": "~3.5",
"@js-draw/material-icons": "1.32.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-community/datetimepicker": "8.4.5",
"@react-native-community/geolocation": "3.4.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-community/push-notification-ios": "1.11.0",
"@react-native-documents/picker": "10.1.5",
"@react-native-documents/picker": "10.1.6",
"@react-native-vector-icons/fontawesome5": "12.3.0",
"@react-native-vector-icons/get-image": "12.3.0",
"@react-native-vector-icons/ionicons": "12.3.0",
"@react-native-vector-icons/material-design-icons": "12.4.0",
"@react-native-vector-icons/material-icons": "12.4.0",
"assert-browserify": "2.0.0",
"buffer": "6.0.3",
"color": "3.2.1",
@@ -45,6 +51,7 @@
"expo-av": "15.1.7",
"expo-camera": "16.1.11",
"expo-local-authentication": "16.0.5",
"js-draw": "1.32.0",
"lodash": "4.17.21",
"md5": "2.3.0",
"path-browserify": "1.0.1",
@@ -66,13 +73,12 @@
"react-native-quick-actions": "0.3.13",
"react-native-quick-crypto": "0.7.17",
"react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "5.5.2",
"react-native-safe-area-context": "5.6.1",
"react-native-securerandom": "1.0.1",
"react-native-share": "12.1.2",
"react-native-share": "12.2.0",
"react-native-sqlite-storage": "6.0.1",
"react-native-svg": "15.13.0",
"react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.3.0",
"react-native-version-info": "1.1.1",
"react-native-webview": "13.15.0",
"react-native-zip-archive": "7.0.2",
@@ -94,7 +100,6 @@
"@joplin/tools": "~3.5",
"@joplin/turndown": "~4.0.80",
"@joplin/turndown-plugin-gfm": "~1.0.62",
"@js-draw/material-icons": "1.30.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"@react-native-community/cli": "16.0.3",
"@react-native-community/cli-platform-android": "16.0.3",
@@ -109,20 +114,19 @@
"@types/node": "18.19.130",
"@types/react": "19.0.14",
"@types/react-redux": "7.1.33",
"@types/serviceworker": "0.0.152",
"@types/serviceworker": "0.0.153",
"@types/tar-stream": "3.1.4",
"babel-jest": "29.7.0",
"babel-loader": "9.1.3",
"babel-plugin-module-resolver": "4.1.0",
"babel-plugin-react-native-web": "0.21.1",
"esbuild": "0.25.9",
"esbuild": "0.25.10",
"fast-deep-equal": "3.1.3",
"fs-extra": "11.2.0",
"fs-extra": "11.3.2",
"gulp": "4.0.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jetifier": "2.0.0",
"js-draw": "1.30.1",
"jsdom": "26.1.0",
"nodemon": "3.1.10",
"punycode": "2.3.1",
@@ -130,7 +134,7 @@
"react-native-web": "0.21.1",
"react-refresh": "0.17.0",
"react-test-renderer": "19.0.0",
"sharp": "0.34.3",
"sharp": "0.34.4",
"sqlite3": "5.1.6",
"timers-browserify": "2.0.12",
"ts-jest": "29.4.1",

View File

@@ -178,6 +178,9 @@ const buildStartupTasks = (
Setting.setConstant('pluginAssetDir', `${Setting.value('resourceDir')}/pluginAssets`);
Setting.setConstant('pluginDir', `${getProfilesRootDir()}/plugins`);
Setting.setConstant('pluginDataDir', getPluginDataDir(currentProfile, isSubProfile));
Setting.setConstant('sync.9.apiKey', '');
Setting.setConstant('sync.10.apiKey', '');
Setting.setConstant('sync.11.apiKey', '');
});
addTask('buildStartupTasks/make resource directory', async () => {
await shim.fsDriver().mkdir(Setting.value('resourceDir'));

View File

@@ -1,29 +1,24 @@
import fontAwesomeFont from 'react-native-vector-icons/Fonts/FontAwesome.ttf';
import fontAwesomeSolidFont from 'react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf';
import fontAwesomeRegularFont from 'react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf';
import fontAwesomeBrandsFont from 'react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf';
import ioniconFont from 'react-native-vector-icons/Fonts/Ionicons.ttf';
import materialCommunityIconsFont from 'react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf';
import antDesignFont from 'react-native-vector-icons/Fonts/AntDesign.ttf';
import fontAwesomeSolidFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Solid.ttf';
import fontAwesomeRegularFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf';
import fontAwesomeBrandsFont from '@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf';
import ioniconFont from '@react-native-vector-icons/ionicons/fonts/Ionicons.ttf';
import materialCommunityIconsFont from '@react-native-vector-icons/material-icons/fonts/MaterialIcons.ttf';
import materialIconsFont from '@react-native-vector-icons/material-design-icons/fonts/MaterialDesignIcons.ttf';
// See https://www.npmjs.com/package/react-native-vector-icons
const setUpRnVectorIcons = () => {
const iconFontStyles = `
@font-face {
src: url(${fontAwesomeFont});
font-family: FontAwesome;
}
@font-face {
src: url(${fontAwesomeSolidFont});
font-family: FontAwesome5_Solid;
font-family: FontAwesome5Free-Solid;
}
@font-face {
src: url(${fontAwesomeRegularFont});
font-family: FontAwesome5_Regular;
font-family: FontAwesome5Free-Regular;
}
@font-face {
src: url(${fontAwesomeBrandsFont});
font-family: FontAwesome5_Brands;
font-family: FontAwesome5Brands-Regular;
}
@font-face {
src: url(${ioniconFont});
@@ -34,8 +29,8 @@ const setUpRnVectorIcons = () => {
font-family: MaterialCommunityIcons;
}
@font-face {
src: url(${antDesignFont});
font-family: AntDesign;
src: url(${materialIconsFont});
font-family: MaterialDesignIcons;
}
`;

View File

@@ -79,8 +79,7 @@ const buildSharedConfig = (hotReload: boolean): webpack.Configuration => {
'@react-native-documents/picker': emptyLibraryMock,
'react-native-exit-app': emptyLibraryMock,
'expo-camera': emptyLibraryMock,
// Remove this after upgrading react-native-vector-icons.
'@react-native-vector-icons/material-design-icons': throwOnLoadLibraryMock,
'react-native-vector-icons/MaterialCommunityIcons': throwOnLoadLibraryMock,
// Workaround for applying serviceworker types to a single file.
// See https://joshuatz.com/posts/2021/strongly-typed-service-workers/.

View File

@@ -19,7 +19,7 @@
},
"dependencies": {
"@joplin/utils": "~3.5",
"fs-extra": "11.2.0",
"fs-extra": "11.3.2",
"yargs": "17.7.2"
}
}

View File

@@ -8,7 +8,7 @@ import {
EditorView, drawSelection, highlightSpecialChars, ViewUpdate, Command, rectangularSelection,
dropCursor,
} from '@codemirror/view';
import { history, undoDepth, redoDepth, standardKeymap, insertTab } from '@codemirror/commands';
import { history, undoDepth, redoDepth, standardKeymap, insertTab, simplifySelection } from '@codemirror/commands';
import { keymap, KeyBinding } from '@codemirror/view';
import { searchKeymap } from '@codemirror/search';
@@ -40,6 +40,7 @@ import ctrlKeyStateClassExtension from './extensions/modifierKeyCssExtension';
import ctrlClickLinksExtension from './extensions/links/ctrlClickLinksExtension';
import { RenderedContentContext } from './extensions/rendering/types';
import ctrlClickCheckboxExtension from './extensions/ctrlClickCheckboxExtension';
import editorSettingsExtension, { setEditorSettingsEffect } from './extensions/editorSettingsExtension';
// Newer versions of CodeMirror by default use Chrome's EditContext API.
// While this might be stable enough for desktop use, it causes significant
@@ -235,6 +236,11 @@ const createEditor = (
}, true),
...standardKeymap, ...historyKeymap, ...searchKeymap,
// The escape -> simplifySelection mapping is present in "defaultKeymap",
// which is disabled on desktop but enabled on mobile. Enable this mapping
// globally for consistency:
keyCommand('Escape', simplifySelection, true),
]));
const editor = new EditorView({
@@ -295,6 +301,7 @@ const createEditor = (
biDirectionalTextExtension,
overwriteModeExtension,
ctrlKeyStateClassExtension,
editorSettingsExtension(settings),
selectedNoteIdExtension,
@@ -340,9 +347,12 @@ const createEditor = (
onSettingsChange: (newSettings: EditorSettings) => {
settings = newSettings;
editor.dispatch({
effects: dynamicConfig.reconfigure(
configFromSettings(newSettings, context),
),
effects: [
dynamicConfig.reconfigure(
configFromSettings(newSettings, context),
),
setEditorSettingsEffect.of(newSettings),
],
});
},
onUndoRedo: () => {

View File

@@ -1,5 +1,6 @@
import { EditorView } from '@codemirror/view';
import { Prec } from '@codemirror/state';
import { editorSettingsFacet } from './editorSettingsExtension';
const hasMultipleCursors = (view: EditorView) => {
return view.state.selection.ranges.length > 1;
@@ -12,7 +13,9 @@ const ctrlClickActionExtension = (onCtrlClick: OnCtrlClick) => {
Prec.high([
EditorView.domEventHandlers({
mousedown: (event: MouseEvent, view: EditorView) => {
const hasModifier = event.ctrlKey || event.metaKey;
const editorSettings = view.state.facet(editorSettingsFacet);
const hasModifier = editorSettings.preferMacShortcuts ? event.metaKey : event.ctrlKey;
// The default CodeMirror action for ctrl-click is to add another cursor
// to the document. If the user already has multiple cursors, assume that
// the ctrl-click action is intended to add another.

View File

@@ -0,0 +1,25 @@
import { Facet, StateEffect, StateField } from '@codemirror/state';
import { EditorSettings } from '../../types';
export const setEditorSettingsEffect = StateEffect.define<EditorSettings>();
export const editorSettingsFacet = Facet.define<EditorSettings|null, EditorSettings|null>({
combine: (possibleValues) => {
return possibleValues.filter(value => !!value)[0] ?? null;
},
});
export default (initialSettings: EditorSettings) => [
StateField.define<EditorSettings|null>({
create: () => initialSettings,
update: (oldValue, transaction) => {
for (const e of transaction.effects) {
if (e.is(setEditorSettingsEffect)) {
return e.value;
}
}
return oldValue;
},
provide: (field) => editorSettingsFacet.from(field),
}),
];

View File

@@ -1,6 +1,9 @@
import { StateEffect, StateField, Transaction } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { editorSettingsFacet } from './editorSettingsExtension';
// On MacOS: Tracks the meta key
// On other platforms: Tracks the ctrl key.
const ctrlOrMetaChangedEffect = StateEffect.define<boolean>();
const ctrlOrMetaPressedField = StateField.define<boolean>({
@@ -18,11 +21,13 @@ const ctrlOrMetaPressedField = StateField.define<boolean>({
})),
...(() => {
const onEvent = (event: KeyboardEvent|MouseEvent, view: EditorView) => {
const ctrlOrCmdPressed = event.ctrlKey || event.metaKey;
if (ctrlOrCmdPressed !== view.state.field(ctrlOrMetaPressedField)) {
const editorSettings = view.state.facet(editorSettingsFacet);
const hasModifier = editorSettings.preferMacShortcuts ? event.metaKey : event.ctrlKey;
if (hasModifier !== view.state.field(ctrlOrMetaPressedField)) {
view.dispatch({
effects: [
ctrlOrMetaChangedEffect.of(ctrlOrCmdPressed),
ctrlOrMetaChangedEffect.of(hasModifier),
],
});
}

View File

@@ -9,12 +9,12 @@ const allowImageUrlsToBeFetched = async () => {
await Promise.resolve();
};
const createEditor = async (initialMarkdown: string, hasImage: boolean) => {
const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['Image']) => {
const resolveImageSrc = jest.fn((src, counter) => Promise.resolve(`${src}?r=${counter}`));
const editor = await createTestEditor(
initialMarkdown,
EditorSelection.cursor(0),
hasImage ? ['Image'] : [],
expectedTags,
[renderBlockImages({ resolveImageSrc })],
);
await allowImageUrlsToBeFetched();
@@ -40,7 +40,7 @@ describe('renderBlockImages', () => {
{ spaceBefore: ' ', spaceAfter: ' ', alt: 'test' },
{ spaceBefore: '', spaceAfter: '', alt: '!!!!' },
])('should render images below their Markdown source (case %#)', async ({ spaceBefore, spaceAfter, alt }) => {
const editor = await createEditor(`${spaceBefore}![${alt}](:/0123456789abcdef0123456789abcdef)${spaceAfter}`, true);
const editor = await createEditor(`${spaceBefore}![${alt}](:/0123456789abcdef0123456789abcdef)${spaceAfter}`);
const images = findImages(editor);
expect(images).toHaveLength(1);
@@ -51,13 +51,13 @@ describe('renderBlockImages', () => {
// For now, only Joplin resources are rendered. This simplifies the implementation and avoids
// potentially-unwanted web requests when opening a note with only the editor open.
test('should not render web images', async () => {
const editor = await createEditor('![test](https://example.com/test.png)\n\n', true);
const editor = await createEditor('![test](https://example.com/test.png)\n\n');
const images = findImages(editor);
expect(images).toHaveLength(0);
});
test('should allow reloading specific images', async () => {
const editor = await createEditor('![test](:/a123456789abcdef0123456789abcdef)\n![test 2](:/b123456789abcdef0123456789abcde2)', true);
const editor = await createEditor('![test](:/a123456789abcdef0123456789abcdef)\n![test 2](:/b123456789abcdef0123456789abcde2)');
// Should have the expected original image URLs
expect(getImageUrls(editor)).toMatchObject([
@@ -98,7 +98,7 @@ describe('renderBlockImages', () => {
const widthAttr = width ? ` width="${width}"` : '';
const editor = await createEditor(
`${spaceBefore}<img src=":/0123456789abcdef0123456789abcdef" alt="${alt}"${widthAttr} />${spaceAfter}`,
false,
['HTMLTag'],
);
const images = findImages(editor);
@@ -117,7 +117,7 @@ describe('renderBlockImages', () => {
test('should render non-self-closing HTML img tags', async () => {
const editor = await createEditor(
'<img src=":/0123456789abcdef0123456789abcdef" alt="test" width="300">',
false,
['HTMLBlock'],
);
const images = findImages(editor);
@@ -128,7 +128,7 @@ describe('renderBlockImages', () => {
test('should not render HTML img tags with web URLs', async () => {
const editor = await createEditor(
'<img src="https://example.com/test.png" alt="test" />',
false,
['HTMLTag'],
);
const images = findImages(editor);
@@ -138,7 +138,7 @@ describe('renderBlockImages', () => {
test('should render both markdown and HTML images in same document', async () => {
const editor = await createEditor(
'![markdown](:/a123456789abcdef0123456789abcdef)\n\n<img src=":/b123456789abcdef0123456789abcde2" alt="html" width="400" />',
true,
['Image', 'HTMLTag'],
);
const images = findImages(editor);
@@ -151,7 +151,7 @@ describe('renderBlockImages', () => {
const editor = await createEditor(
// eslint-disable-next-line quotes
"<img src=':/0123456789abcdef0123456789abcdef' alt='test' width='250' />",
false,
['HTMLTag'],
);
const images = findImages(editor);

View File

@@ -15,6 +15,7 @@ import jumpToHash from '../utils/jumpToHash';
import focusEditor from './focusEditor';
import canReplaceSelectionWith from '../utils/canReplaceSelectionWith';
import showCreateEditablePrompt from '../plugins/joplinEditablePlugin/showCreateEditablePrompt';
import getTextBetween from '../utils/getTextBetween';
type Dispatch = (tr: Transaction)=> void;
type ExtendedCommand = (state: EditorState, dispatch: Dispatch, view?: EditorView, options?: string[])=> boolean;
@@ -86,13 +87,20 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
[EditorCommandType.ToggleItalicized]: toggleMark(schema.marks.emphasis),
[EditorCommandType.ToggleCode]: toggleCode,
[EditorCommandType.ToggleMath]: (state, dispatch, view) => {
const selectedText = state.doc.textBetween(state.selection.from, state.selection.to);
const block = selectedText.includes('\n');
const nodeType = block ? schema.nodes.joplinEditableBlock : schema.nodes.joplinEditableInline;
const inlineNodeType = schema.nodes.joplinEditableInline;
const blockNodeType = schema.nodes.joplinEditableBlock;
// If multiple paragraphs are selected, it usually isn't possible to replace them
// to inline math. Fall back to block math:
const block = !canReplaceSelectionWith(state.selection, inlineNodeType);
const nodeType = block ? blockNodeType : inlineNodeType;
if (canReplaceSelectionWith(state.selection, nodeType)) {
if (view) {
return showCreateEditablePrompt(block ? '$$\n\t...\n$$' : '$...$', !block)(state, dispatch, view);
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
const content = selectedText || '...';
return showCreateEditablePrompt(
block ? `$$\n\t${content}\n$$` : `$${content}$`, !block,
)(state, dispatch, view);
}
return true;
}
@@ -135,7 +143,10 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
return true;
},
[EditorCommandType.InsertCodeBlock]: (state, dispatch, view) => {
return showCreateEditablePrompt('```\n\n```', false)(state, dispatch, view);
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
return showCreateEditablePrompt(
`\`\`\`\n${selectedText}\n\`\`\``, false,
)(state, dispatch, view);
},
[EditorCommandType.ToggleSearch]: (state, dispatch, view) => {
const command = setSearchVisible(!getSearchVisible(state));

View File

@@ -1,4 +1,4 @@
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, tableEditing } from 'prosemirror-tables';
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, deleteTable, tableEditing } from 'prosemirror-tables';
import createFloatingButtonPlugin, { ToolbarType } from './utils/createFloatingButtonPlugin';
import addColumnRightIcon from '../vendor/icons/addColumnRight';
import addRowBelowIcon from '../vendor/icons/addRowBelow';
@@ -11,6 +11,13 @@ const tableCommand = (command: Command): Command => (state, dispatch, view) => {
return command(state, dispatch, view) && focusEditor(state, dispatch, view);
};
// By default, commands like deleteRow or deleteColumn don't delete the last
// row/column in the table. This command removes the table when there are no more
// rows/columns to delete:
const runCommandOrDeleteTable = (command: Command): Command => (state, dispatch, view) => {
return command(state, dispatch, view) || deleteTable(state, dispatch);
};
const tablePlugin = [
tableEditing({ allowTableNodeSelection: true }),
createFloatingButtonPlugin('table', [
@@ -27,12 +34,12 @@ const tablePlugin = [
{
icon: removeRowIcon,
label: (_) => _('Delete row'),
command: () => tableCommand(deleteRow),
command: () => tableCommand(runCommandOrDeleteTable(deleteRow)),
},
{
icon: removeColumnIcon,
label: (_) => _('Delete column'),
command: () => tableCommand(deleteColumn),
command: () => tableCommand(runCommandOrDeleteTable(deleteColumn)),
},
], ToolbarType.FloatAboveBelow),
];

View File

@@ -161,6 +161,7 @@ const nodes = addDefaultToplevelAttributes({
inline: true,
group: 'inlineBreak',
selectable: false,
leafText: () => '\n',
parseDOM: [{ tag: 'br' }],
toDOM: () => domOutputSpecs.br,
},

View File

@@ -0,0 +1,8 @@
import { Node } from 'prosemirror-model';
const getTextBetween = (doc: Node, from: number, to: number) => {
const blockSeparator = '\n\n';
return doc.textBetween(from, to, blockSeparator);
};
export default getTextBetween;

View File

@@ -31,4 +31,30 @@ describe('postprocessEditorOutput', () => {
`),
);
});
// Removing extra space around checklist item content prevents extra space from being
// added when converting from HTML to Markdown
test('should remove wrapper paragraphs from around checklist items', () => {
const doc = new DOMParser().parseFromString(`
<body>
<ul>
<li><input><div><p>Should remove single wrapper paragraphs to avoid extra newlines when saving as Markdown.</p></div></li>
<li><input><div><p>Should not remove paragraphs...</p><p>...when there are multiple.</p></div></li>
</ul>
</body>
`, 'text/html');
const output = postprocessEditorOutput(doc.body);
expect(
normalizeHtmlString(output.querySelector('ul').outerHTML),
).toBe(
normalizeHtmlString(`
<ul>
<li><input><span>Should remove single wrapper paragraphs to avoid extra newlines when saving as Markdown.</span></li>
<li><input><div><p>Should not remove paragraphs...</p><p>...when there are multiple.</p></div></li>
</ul>
`),
);
});
});

View File

@@ -13,6 +13,7 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
for (const item of listItems) {
trimEmptyParagraphs(item);
// Replace <li><p>...text...</p></li> with <li>...text...</li>
if (item.children.length === 1) {
const firstChild = item.children[0];
if (firstChild.tagName === 'P') {
@@ -22,6 +23,30 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
}
};
// Avoids extra newlines from being included in the output Markdown
const removeChecklistItemWrapperParagraphs = (container: HTMLElement) => {
const listItems = container.querySelectorAll<HTMLLIElement>('li');
for (const item of listItems) {
// Is it a checklist item?
if (item.children.length !== 2) continue;
const input = item.children[0];
const content = item.children[1];
if (input.tagName !== 'INPUT' || content.tagName !== 'DIV') continue;
trimEmptyParagraphs(content);
// Replace <li><input/><div><p>...text...</p></div></li> with <li><input/><span>...text...</span></li>
if (content.children.length === 1) {
const firstChild = content.children[0];
if (firstChild.tagName === 'P') {
const newContent = document.createElement('span');
newContent.replaceChildren(...firstChild.childNodes);
content.replaceWith(newContent);
}
}
}
};
const restoreOriginalLinks = (container: HTMLElement) => {
// Restore HREFs
const links = container.querySelectorAll<HTMLAnchorElement>('a[href="#"][data-original-href]');
@@ -63,6 +88,7 @@ const postprocessEditorOutput = (node: Node|DocumentFragment) => {
fixResourceUrls(html);
restoreOriginalLinks(html);
removeListItemWrapperParagraphs(html);
removeChecklistItemWrapperParagraphs(html);
removeTableItemExtraPadding(html);
return html;

View File

@@ -44,7 +44,7 @@
"@lezer/highlight": "1.2.1",
"@lezer/markdown": "1.3.2",
"@replit/codemirror-vim": "6.2.1",
"dompurify": "3.2.6",
"dompurify": "3.2.7",
"orderedmap": "2.1.1",
"prosemirror-commands": "1.7.1",
"prosemirror-dropcursor": "1.8.2",

Some files were not shown because too many files have changed in this diff Show More