You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-04-18 19:42:23 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07f8fe3966 |
+16
-18
@@ -4,11 +4,9 @@ reviews:
|
||||
high_level_summary: false
|
||||
estimate_code_review_effort: false
|
||||
poem: false
|
||||
review_status: false
|
||||
review_details: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: true
|
||||
drafts: false
|
||||
ignore_usernames:
|
||||
- "renovate[bot]"
|
||||
auto_apply_labels: true
|
||||
@@ -88,21 +86,21 @@ reviews:
|
||||
- label: "windows"
|
||||
instructions: "Apply when the PR is mainly about changes specific to Windows"
|
||||
|
||||
# pre_merge_checks:
|
||||
# description:
|
||||
# mode: "warning"
|
||||
# custom_checks:
|
||||
# - name: "PR Description Must Follow Guidelines"
|
||||
# mode: "error"
|
||||
# instructions: |
|
||||
# Fail if the pull request description does not include clear sections for:
|
||||
# - Problem or user-impact description
|
||||
# - A high-level Solution explanation
|
||||
# - Any Test Plan or verification steps
|
||||
#
|
||||
# The description should align with our PR guidelines
|
||||
# at https://github.com/joplin/gsoc/blob/master/pull_request_guidelines.md
|
||||
# and should not just restate the diff or implementation details.
|
||||
pre_merge_checks:
|
||||
description:
|
||||
mode: "warning"
|
||||
custom_checks:
|
||||
- name: "PR Description Must Follow Guidelines"
|
||||
mode: "error"
|
||||
instructions: |
|
||||
Fail if the pull request description does not include clear sections for:
|
||||
- Problem or user-impact description
|
||||
- A high-level Solution explanation
|
||||
- Any Test Plan or verification steps
|
||||
|
||||
The description should align with our PR guidelines
|
||||
at https://github.com/joplin/gsoc/blob/master/pull_request_guidelines.md
|
||||
and should not just restate the diff or implementation details.
|
||||
knowledge_base:
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
|
||||
+4
-13
@@ -103,7 +103,6 @@ packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
packages/app-cli/app/command-cat.js
|
||||
packages/app-cli/app/command-clear.js
|
||||
packages/app-cli/app/command-config.js
|
||||
packages/app-cli/app/command-cp.js
|
||||
packages/app-cli/app/command-done.test.js
|
||||
@@ -201,8 +200,6 @@ packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
|
||||
@@ -360,6 +357,8 @@ packages/app-desktop/gui/NoteList/utils/useMoveNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteDoubleClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.js
|
||||
packages/app-desktop/gui/NoteList/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
@@ -536,6 +535,8 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useCtrlWheelZoom.test.js
|
||||
packages/app-desktop/gui/hooks/useCtrlWheelZoom.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -698,15 +699,10 @@ packages/app-mobile/components/EditorToolbar/utils/isSelected.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useButtonSize.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.test.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.test.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/polyfillScrollFunctions.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
@@ -877,7 +873,6 @@ packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/TextWrapCalculator.js
|
||||
packages/app-mobile/components/screens/ResourceScreen.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
@@ -894,8 +889,6 @@ packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/folder.js
|
||||
packages/app-mobile/components/screens/resourceScreenUtils.test.js
|
||||
packages/app-mobile/components/screens/resourceScreenUtils.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
@@ -966,7 +959,6 @@ packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/appReducer.test.js
|
||||
packages/app-mobile/utils/appReducer.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
@@ -1451,7 +1443,6 @@ packages/lib/services/CommandService.test.js
|
||||
packages/lib/services/CommandService.js
|
||||
packages/lib/services/DecryptionWorker.test.js
|
||||
packages/lib/services/DecryptionWorker.js
|
||||
packages/lib/services/ExternalEditWatcher.test.js
|
||||
packages/lib/services/ExternalEditWatcher.js
|
||||
packages/lib/services/ExternalEditWatcher/utils.js
|
||||
packages/lib/services/ItemChangeUtils.js
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
name: react-native-android-build-apk
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
AssembleRelease:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Build macOS M1
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Main:
|
||||
# We always process desktop release tags, because they also publish the release
|
||||
|
||||
@@ -4,6 +4,6 @@ jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Slashgear/action-check-pr-title@v5.0.1
|
||||
- uses: Slashgear/action-check-pr-title@v4.3.0
|
||||
with:
|
||||
regexp: "(Desktop|Mobile|All|Cli|Tools|Chore|Clipper|Server|Android|iOS|Plugins|CI|Plugin Repo|Doc): (Fixes|Resolves) #[0-9]+: .+"
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.6.1
|
||||
uses: contributor-assistant/github-action@v2.3.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Joplin Continuous Integration
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Main:
|
||||
# We always process server or desktop release tags, because they also publish the release
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Joplin UI tests
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref == 'refs/heads/dev' && github.run_id || format('{0}-{1}', github.workflow, github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
|
||||
+4
-13
@@ -76,7 +76,6 @@ packages/app-cli/app/command-apidoc.js
|
||||
packages/app-cli/app/command-attach.js
|
||||
packages/app-cli/app/command-batch.js
|
||||
packages/app-cli/app/command-cat.js
|
||||
packages/app-cli/app/command-clear.js
|
||||
packages/app-cli/app/command-config.js
|
||||
packages/app-cli/app/command-cp.js
|
||||
packages/app-cli/app/command-done.test.js
|
||||
@@ -174,8 +173,6 @@ packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.test.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/GlobalHotkeyInput.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
|
||||
@@ -333,6 +330,8 @@ packages/app-desktop/gui/NoteList/utils/useMoveNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteDoubleClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRefocusOnDeletion.js
|
||||
packages/app-desktop/gui/NoteList/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
@@ -509,6 +508,8 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useCtrlWheelZoom.test.js
|
||||
packages/app-desktop/gui/hooks/useCtrlWheelZoom.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -671,15 +672,10 @@ packages/app-mobile/components/EditorToolbar/utils/isSelected.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/selectedCommandNamesFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/toolbarButtonsFromState.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useButtonSize.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.test.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useSaveToolbarButtons.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.test.js
|
||||
packages/app-mobile/components/EditorToolbar/utils/useToolbarEditorState.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.jest.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.js
|
||||
packages/app-mobile/components/ExtendedWebView/index.web.js
|
||||
packages/app-mobile/components/ExtendedWebView/types.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/polyfillScrollFunctions.js
|
||||
packages/app-mobile/components/ExtendedWebView/utils/useCss.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Icon.js
|
||||
@@ -850,7 +846,6 @@ packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/TextWrapCalculator.js
|
||||
packages/app-mobile/components/screens/ResourceScreen.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
@@ -867,8 +862,6 @@ packages/app-mobile/components/screens/dropbox-login.js
|
||||
packages/app-mobile/components/screens/encryption-config.test.js
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/folder.js
|
||||
packages/app-mobile/components/screens/resourceScreenUtils.test.js
|
||||
packages/app-mobile/components/screens/resourceScreenUtils.js
|
||||
packages/app-mobile/components/screens/status.js
|
||||
packages/app-mobile/components/screens/tags.js
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
@@ -939,7 +932,6 @@ packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/appDefaultState.js
|
||||
packages/app-mobile/utils/appReducer.test.js
|
||||
packages/app-mobile/utils/appReducer.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
packages/app-mobile/utils/buildStartupTasks.js
|
||||
@@ -1424,7 +1416,6 @@ packages/lib/services/CommandService.test.js
|
||||
packages/lib/services/CommandService.js
|
||||
packages/lib/services/DecryptionWorker.test.js
|
||||
packages/lib/services/DecryptionWorker.js
|
||||
packages/lib/services/ExternalEditWatcher.test.js
|
||||
packages/lib/services/ExternalEditWatcher.js
|
||||
packages/lib/services/ExternalEditWatcher/utils.js
|
||||
packages/lib/services/ItemChangeUtils.js
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
- Focus on testing essential behaviour and edge cases — avoid adding tests for every minor detail.
|
||||
- Avoid duplicating code in tests; when testing the same logic with different inputs, use `test.each` or shared helpers instead of repeating similar test blocks.
|
||||
- Do not make white space changes - do not add unnecessary new lines, or spaces to existing code, or wrap existing code.
|
||||
- If you add a new TypeScript file, run `yarn updateIgnored` from the root.
|
||||
- When an unknown word is detected by cSpell, handle is as per the specification in `readme/dev/spellcheck.md`
|
||||
- To compile TypeScript, use `yarn tsc`. To type-check without emitting files, use `yarn tsc --noEmit`.
|
||||
|
||||
## Full Documentation
|
||||
|
||||
|
||||
+2
-2
@@ -11,13 +11,13 @@
|
||||
},
|
||||
"nodejs": "24.11.1",
|
||||
"pkg-config": "latest",
|
||||
"python": "3.14.0",
|
||||
"python": "3.13.3",
|
||||
"bat": "latest",
|
||||
"electron": {
|
||||
"version": "latest",
|
||||
"excluded_platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"git": "2.51.2",
|
||||
"git": "2.51.0",
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
|
||||
+1
-5
@@ -72,7 +72,6 @@
|
||||
"@crowdin/cli": "4",
|
||||
"@joplin/utils": "~2.12",
|
||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||
"@types/jest": "29.5.14",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"cspell": "5.21.2",
|
||||
@@ -83,7 +82,7 @@
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "11.1.0",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
@@ -102,9 +101,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.41.0",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
|
||||
"eslint": "patch:eslint@8.57.1#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
|
||||
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
|
||||
|
||||
+29
-24
@@ -1,7 +1,7 @@
|
||||
import BaseApplication from '@joplin/lib/BaseApplication';
|
||||
import { refreshFolders } from '@joplin/lib/folders-screen-utils.js';
|
||||
import ResourceService from '@joplin/lib/services/ResourceService';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -15,22 +15,20 @@ import RevisionService from '@joplin/lib/services/RevisionService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import setupCommand from './setupCommand';
|
||||
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
type FolderOrNoteType = ModelType.Note | ModelType.Folder | 'folderOrNote';
|
||||
import initializeCommandService from './utils/initializeCommandService';
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const Cache = require('@joplin/lib/Cache');
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command loading system
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private commands_: Record<string, any> = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command metadata
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private commandMetadata_: any = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command type
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private activeCommand_: any = null;
|
||||
private allCommandsLoaded_ = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic GUI type with many optional methods
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private gui_: any = null;
|
||||
private cache_ = new Cache();
|
||||
|
||||
@@ -42,16 +40,18 @@ class Application extends BaseApplication {
|
||||
return this.gui().stdoutMaxWidth();
|
||||
}
|
||||
|
||||
public async guessTypeAndLoadItem(pattern: string, options: { parent?: FolderEntity } | null = null) {
|
||||
let type: FolderOrNoteType = ModelType.Note;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async guessTypeAndLoadItem(pattern: string, options: any = null) {
|
||||
let type = BaseModel.TYPE_NOTE;
|
||||
if (pattern.indexOf('/') === 0) {
|
||||
type = ModelType.Folder;
|
||||
type = BaseModel.TYPE_FOLDER;
|
||||
pattern = pattern.substr(1);
|
||||
}
|
||||
return this.loadItem(type, pattern, options);
|
||||
}
|
||||
|
||||
public async loadItem(type: FolderOrNoteType, pattern: string, options: { parent?: FolderEntity } | null = null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async loadItem(type: ModelType | 'folderOrNote', pattern: string, options: any = null) {
|
||||
const output = await this.loadItems(type, pattern, options);
|
||||
|
||||
if (output.length > 1) {
|
||||
@@ -75,36 +75,37 @@ class Application extends BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
public async loadItemOrFail(type: FolderOrNoteType, pattern: string) {
|
||||
public async loadItemOrFail(type: ModelType | 'folderOrNote', pattern: string) {
|
||||
const output = await this.loadItem(type, pattern);
|
||||
if (!output) throw new Error(_('Cannot find "%s".', pattern));
|
||||
return output;
|
||||
}
|
||||
|
||||
public async loadItems(type: FolderOrNoteType, pattern: string, options: { parent?: FolderEntity } | null = null): Promise<(FolderEntity | NoteEntity)[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async loadItems(type: ModelType | 'folderOrNote', pattern: string, options: any = null): Promise<(FolderEntity | NoteEntity)[]> {
|
||||
if (type === 'folderOrNote') {
|
||||
const folders: FolderEntity[] = await this.loadItems(ModelType.Folder, pattern, options);
|
||||
const folders: FolderEntity[] = await this.loadItems(BaseModel.TYPE_FOLDER, pattern, options);
|
||||
if (folders.length) return folders;
|
||||
return await this.loadItems(ModelType.Note, pattern, options);
|
||||
return await this.loadItems(BaseModel.TYPE_NOTE, pattern, options);
|
||||
}
|
||||
|
||||
pattern = pattern ? pattern.toString() : '';
|
||||
|
||||
if (type === ModelType.Folder && (pattern === Folder.conflictFolderTitle() || pattern === Folder.conflictFolderId())) return [Folder.conflictFolder()];
|
||||
if (type === BaseModel.TYPE_FOLDER && (pattern === Folder.conflictFolderTitle() || pattern === Folder.conflictFolderId())) return [Folder.conflictFolder()];
|
||||
|
||||
if (!options) options = {};
|
||||
|
||||
const parent = options.parent ? options.parent : app().currentFolder();
|
||||
const ItemClass = BaseItem.itemClass(type);
|
||||
|
||||
if (type === ModelType.Note && pattern.indexOf('*') >= 0) {
|
||||
if (type === BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) {
|
||||
// Handle it as pattern
|
||||
if (!parent) throw new Error(_('No notebook selected.'));
|
||||
return await Note.previews(parent.id, { titlePattern: pattern });
|
||||
} else {
|
||||
// Single item
|
||||
let item = null;
|
||||
if (type === ModelType.Note) {
|
||||
if (type === BaseModel.TYPE_NOTE) {
|
||||
if (!parent) throw new Error(_('No notebook has been specified.'));
|
||||
item = await (ItemClass as typeof Note).loadFolderNoteByField(parent.id, 'title', pattern);
|
||||
} else {
|
||||
@@ -171,7 +172,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
if (uiType !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dynamic command type
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const temp: Record<string, any> = {};
|
||||
for (const n in this.commands_) {
|
||||
if (!this.commands_.hasOwnProperty(n)) continue;
|
||||
@@ -232,7 +233,8 @@ class Application extends BaseApplication {
|
||||
CommandClass = require(`${__dirname}/command-${name}.js`);
|
||||
} catch (error) {
|
||||
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
|
||||
const e: Error & { type?: string } = new Error(_('No such command: %s', name));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const e: any = new Error(_('No such command: %s', name));
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
} else {
|
||||
@@ -251,7 +253,8 @@ class Application extends BaseApplication {
|
||||
isDummy: () => {
|
||||
return true;
|
||||
},
|
||||
prompt: (initialText = '', promptString = '', options: Record<string, unknown> | null = null) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
prompt: (initialText = '', promptString = '', options: any = null) => {
|
||||
return cliUtils.prompt(initialText, promptString, options);
|
||||
},
|
||||
showConsole: () => {},
|
||||
@@ -273,7 +276,8 @@ class Application extends BaseApplication {
|
||||
};
|
||||
}
|
||||
|
||||
public async execCommand(argv: string[]): Promise<void> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async execCommand(argv: string[]): Promise<any> {
|
||||
if (!argv.length) return this.execCommand(['help']);
|
||||
// reg.logger().debug('execCommand()', argv);
|
||||
const commandName = argv[0];
|
||||
@@ -392,7 +396,8 @@ class Application extends BaseApplication {
|
||||
const keychainEnabled = this.checkIfKeychainEnabled(argv);
|
||||
argv = await super.start(argv, { keychainEnabled });
|
||||
|
||||
cliUtils.setStdout((object: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
cliUtils.setStdout((object: any) => {
|
||||
return this.stdout(object);
|
||||
});
|
||||
|
||||
@@ -443,7 +448,7 @@ class Application extends BaseApplication {
|
||||
this.gui_.setLogger(this.logger());
|
||||
await this.gui_.start();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Redux dispatch type requires AnyAction
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
await refreshFolders((action: any) => this.store().dispatch(action), '');
|
||||
|
||||
const tags = await Tag.allWithNotes();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -17,7 +17,7 @@ class Command extends BaseCommand {
|
||||
public override async action(args: any) {
|
||||
const title = args['note'];
|
||||
|
||||
const note = await app().loadItem(ModelType.Note, title, { parent: app().currentFolder() });
|
||||
const note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
||||
this.encryptionCheck(note);
|
||||
if (!note) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
@@ -22,7 +22,7 @@ class Command extends BaseCommand {
|
||||
public override async action(args: any) {
|
||||
const title = args['note'];
|
||||
|
||||
const item = await app().loadItem(ModelType.Note, title, { parent: app().currentFolder() });
|
||||
const item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
||||
if (!item) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
let content = '';
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
return 'clear';
|
||||
}
|
||||
|
||||
public override description() {
|
||||
return _('Clears the console output.');
|
||||
}
|
||||
|
||||
public override async action() {
|
||||
app().gui().widget('console').clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -17,14 +17,14 @@ class Command extends BaseCommand {
|
||||
public override async action(args: any) {
|
||||
let folder = null;
|
||||
if (args['notebook']) {
|
||||
folder = await app().loadItem(ModelType.Folder, args['notebook']);
|
||||
folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
|
||||
} else {
|
||||
folder = app().currentFolder();
|
||||
}
|
||||
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
|
||||
|
||||
const notes = await app().loadItems(ModelType.Note, args['note']);
|
||||
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args['note']);
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', args['note']));
|
||||
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import time from '@joplin/lib/time';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
@@ -17,7 +17,7 @@ class Command extends BaseCommand {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public static async handleAction(commandInstance: BaseCommand, args: any, isCompleted: boolean) {
|
||||
const note: NoteEntity = await app().loadItem(ModelType.Note, args.note);
|
||||
const note: NoteEntity = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
|
||||
commandInstance.encryptionCheck(note);
|
||||
if (!note) throw new Error(_('Cannot find "%s".', args.note));
|
||||
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
|
||||
|
||||
@@ -6,7 +6,7 @@ import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
@@ -39,7 +39,7 @@ class Command extends BaseCommand {
|
||||
const title = args['note'];
|
||||
|
||||
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
|
||||
let note = await app().loadItem(ModelType.Note, title);
|
||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
|
||||
|
||||
this.encryptionCheck(note);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BaseCommand from './base-command';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ExportOptions } from '@joplin/lib/services/interop/types';
|
||||
@@ -34,12 +34,12 @@ class Command extends BaseCommand {
|
||||
if (exportOptions.format === 'html') throw new Error('HTML export is not supported. Please use the desktop application.');
|
||||
|
||||
if (args.options.note) {
|
||||
const notes = await app().loadItems(ModelType.Note, args.options.note, { parent: app().currentFolder() });
|
||||
const notes = await app().loadItems(BaseModel.TYPE_NOTE, args.options.note, { parent: app().currentFolder() });
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', args.options.note));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
exportOptions.sourceNoteIds = notes.map((n: any) => n.id);
|
||||
} else if (args.options.notebook) {
|
||||
const folders = await app().loadItems(ModelType.Folder, args.options.notebook);
|
||||
const folders = await app().loadItems(BaseModel.TYPE_FOLDER, args.options.notebook);
|
||||
if (!folders.length) throw new Error(_('Cannot find "%s".', args.options.notebook));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
exportOptions.sourceFolderIds = folders.map((n: any) => n.id);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -17,7 +17,7 @@ class Command extends BaseCommand {
|
||||
public override async action(args: any) {
|
||||
const title = args['note'];
|
||||
|
||||
const item = await app().loadItem(ModelType.Note, title, { parent: app().currentFolder() });
|
||||
const item = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
||||
if (!item) throw new Error(_('Cannot find "%s".', title));
|
||||
const url = Note.geolocationUrl(item);
|
||||
this.stdout(url);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BaseCommand from './base-command';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
@@ -33,7 +33,7 @@ class Command extends BaseCommand {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public override async action(args: any) {
|
||||
let destinationFolder = await app().loadItem(ModelType.Folder, args.notebook);
|
||||
let destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook);
|
||||
|
||||
if (args.notebook && !destinationFolder) throw new Error(_('Cannot find "%s".', args.notebook));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const BaseCommand = require('./base-command').default;
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
@@ -23,7 +23,7 @@ class Command extends BaseCommand {
|
||||
// validDestinationFolder check for presents and ambiguous folders
|
||||
public async validDestinationFolder(targetFolder: string) {
|
||||
|
||||
const destinationFolder = await app().loadItem(ModelType.Folder, targetFolder);
|
||||
const destinationFolder = await app().loadItem(BaseModel.TYPE_FOLDER, targetFolder);
|
||||
if (!destinationFolder) {
|
||||
throw new Error(_('Cannot find: "%s"', targetFolder));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
@@ -21,7 +21,7 @@ class Command extends BaseCommand {
|
||||
let folder = null;
|
||||
|
||||
if (destination !== 'root') {
|
||||
folder = await app().loadItem(ModelType.Folder, destination);
|
||||
folder = await app().loadItem(BaseModel.TYPE_FOLDER, destination);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', destination));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class Command extends BaseCommand {
|
||||
throw new Error(_('Ambiguous notebook "%s". Please use short notebook id instead - press "ti" to see the short notebook id', destination));
|
||||
}
|
||||
|
||||
const itemFolder = await app().loadItem(ModelType.Folder, pattern);
|
||||
const itemFolder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
if (itemFolder) {
|
||||
const sourceDuplicates = await Folder.search({ titlePattern: pattern, limit: 2 });
|
||||
if (sourceDuplicates.length > 1) {
|
||||
@@ -42,7 +42,7 @@ class Command extends BaseCommand {
|
||||
await Folder.moveToFolder(itemFolder.id, folder.id);
|
||||
}
|
||||
} else {
|
||||
const notes = await app().loadItems(ModelType.Note, pattern);
|
||||
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
|
||||
if (notes.length === 0) throw new Error(_('Cannot find "%s".', pattern));
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
await Note.moveToFolder(notes[i].id, folder.id);
|
||||
|
||||
@@ -2,7 +2,7 @@ import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -26,7 +26,7 @@ class Command extends BaseCommand {
|
||||
const pattern = args['notebook'];
|
||||
const force = args.options && args.options.force === true;
|
||||
|
||||
const folder = await app().loadItemOrFail(ModelType.Folder, pattern);
|
||||
const folder = await app().loadItemOrFail(BaseModel.TYPE_FOLDER, pattern);
|
||||
|
||||
const permanent = args.options?.permanent === true || !!folder.deleted_time;
|
||||
const ellipsizedFolderTitle = substrWithEllipsis(folder.title, 0, 32);
|
||||
|
||||
@@ -2,7 +2,7 @@ import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { DeleteOptions, ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel, { DeleteOptions } from '@joplin/lib/BaseModel';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -26,7 +26,7 @@ class Command extends BaseCommand {
|
||||
const pattern = args['note-pattern'];
|
||||
const force = args.options && args.options.force === true;
|
||||
|
||||
const notes: NoteEntity[] = await app().loadItems(ModelType.Note, pattern);
|
||||
const notes: NoteEntity[] = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
|
||||
|
||||
let ok = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Database from '@joplin/lib/database';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
@@ -29,7 +29,7 @@ class Command extends BaseCommand {
|
||||
let propValue = args['value'];
|
||||
if (!propValue) propValue = '';
|
||||
|
||||
const notes = await app().loadItems(ModelType.Note, title);
|
||||
const notes = await app().loadItems(BaseModel.TYPE_NOTE, title);
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -19,7 +19,7 @@ class Command extends BaseCommand {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public override async action(args: any) {
|
||||
const folder = await app().loadItem(ModelType.Folder, args['notebook']);
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, args['notebook']);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', args['notebook']));
|
||||
|
||||
// Auto-expand parent folders in GUI if present
|
||||
|
||||
@@ -34,12 +34,6 @@ class ConsoleWidget extends TextWidget {
|
||||
super.onBlur();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.lines_ = [];
|
||||
this.updateText_ = true;
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.updateText_) {
|
||||
if (this.lines_.length > this.maxLines_) {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "11.3.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"html-entities": "1.4.0",
|
||||
"keytar": "7.9.0",
|
||||
"md5": "2.3.0",
|
||||
|
||||
@@ -338,7 +338,7 @@ describe('MdToHtml', () => {
|
||||
for (const [tex, input] of tests) {
|
||||
const html = await mdToHtml.render(input, null, { bodyOnly: true });
|
||||
|
||||
const opening = '<pre class="joplin-source" hidden data-joplin-language="katex" data-joplin-source-open="$$ " data-joplin-source-close=" $$ ">';
|
||||
const opening = '<pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$ " data-joplin-source-close=" $$ ">';
|
||||
const closing = '</pre>';
|
||||
|
||||
// Remove any single leading and trailing newlines, those are included in data-joplin-source-open
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<div class="joplin-editable joplin-abc-notation">
|
||||
<pre class="joplin-source" hidden data-abc-options="{"responsive":"resize"}" data-joplin-language="abc" data-joplin-source-open="```abc " data-joplin-source-close=" ``` ">{responsive:'resize'}
|
||||
<pre class="joplin-source" data-abc-options="{"responsive":"resize"}" data-joplin-language="abc" data-joplin-source-open="```abc " data-joplin-source-close=" ``` ">{responsive:'resize'}
|
||||
---
|
||||
K:F
|
||||
!f!(fgag-g2c2)|</pre>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="joplin-editable"><pre class="joplin-source" hidden data-joplin-language="javascript" data-joplin-source-open="```javascript " data-joplin-source-close=" ```">function() {
|
||||
<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="javascript" data-joplin-source-open="```javascript " data-joplin-source-close=" ```">function() {
|
||||
console.info('bonjour');
|
||||
}</pre><pre class="hljs"><code><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) {
|
||||
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">info</span>(<span class="hljs-string">'bonjour'</span>);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<div class="joplin-editable">
|
||||
<span class="joplin-source" hidden data-joplin-source-open="" data-joplin-source-close="">https://www.youtube.com/watch?v=iJqe9pC-z-Y</span>
|
||||
<span class="joplin-source" data-joplin-source-open="" data-joplin-source-close="">https://www.youtube.com/watch?v=iJqe9pC-z-Y</span>
|
||||
<div class="joplin-youtube-player-rendered">
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/iJqe9pC-z-Y" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<p>Link: <a data-from-md title='https://www.youtube.com/watch?v=iJqe9pC-z-Y' href='https://www.youtube.com/watch?v=iJqe9pC-z-Y' onclick='postMessage("https://www.youtube.com/watch?v=iJqe9pC-z-Y", { resourceId: "" }); return false;'>https://www.youtube.com/watch?v=iJqe9pC-z-Y</a></p>
|
||||
<p>
|
||||
<div class="joplin-editable">
|
||||
<span class="joplin-source" hidden data-joplin-source-open="" data-joplin-source-close="">https://www.youtube.com/watch?v=iJqe9pC-z-Y</span>
|
||||
<span class="joplin-source" data-joplin-source-open="" data-joplin-source-close="">https://www.youtube.com/watch?v=iJqe9pC-z-Y</span>
|
||||
<div class="joplin-youtube-player-rendered">
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/iJqe9pC-z-Y" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<div class="joplin-editable"><pre class="joplin-source" hidden data-joplin-language=""><svg/onload=top.eval(atob("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp"))>" data-joplin-source-open="```"><svg/onload=top.eval(atob("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp"))> " data-joplin-source-close=" ```">ts</pre><pre class="hljs"><code><span class="hljs-attribute">ts</span></code></pre></div>
|
||||
<div class="joplin-editable"><pre class="joplin-source" data-joplin-language=""><svg/onload=top.eval(atob("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp"))>" data-joplin-source-open="```"><svg/onload=top.eval(atob("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp"))> " data-joplin-source-close=" ```">ts</pre><pre class="hljs"><code><span class="hljs-attribute">ts</span></code></pre></div>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<div class="joplin-editable"><pre class="joplin-source" hidden data-joplin-language="html" data-joplin-source-open="```html " data-joplin-source-close=" ```"><a href="#" onclick="leavethisalone">testing fence</a></pre><pre class="hljs"><code><span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"leavethisalone"</span>></span>testing fence<span class="hljs-tag"></<span class="hljs-name">a</span>></span></code></pre></div>
|
||||
<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="html" data-joplin-source-open="```html " data-joplin-source-close=" ```"><a href="#" onclick="leavethisalone">testing fence</a></pre><pre class="hljs"><code><span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"leavethisalone"</span>></span>testing fence<span class="hljs-tag"></<span class="hljs-name">a</span>></span></code></pre></div>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -297,11 +297,7 @@ class AppComponent extends Component {
|
||||
if (!this.state.contentScriptLoaded) {
|
||||
let msg = 'Loading...';
|
||||
if (this.state.contentScriptError) msg = `The Joplin extension is not available on this tab due to: ${this.state.contentScriptError}`;
|
||||
return (
|
||||
<div className="App Startup">
|
||||
{msg}
|
||||
</div>
|
||||
);
|
||||
return <div style={{ padding: 10, fontSize: 12, maxWidth: 200 }}>{msg}</div>;
|
||||
}
|
||||
|
||||
const warningComponent = !this.props.warning ? null : <div className="Warning">{ this.props.warning }</div>;
|
||||
|
||||
@@ -6,7 +6,7 @@ const shim: typeof ShimType = require('@joplin/lib/shim').default;
|
||||
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import { FileLocker } from '@joplin/utils/fs';
|
||||
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
|
||||
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme, Menu, session as electronSession, Session } from 'electron';
|
||||
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme, powerMonitor } from 'electron';
|
||||
import bridge from './bridge';
|
||||
import * as url from 'url';
|
||||
const path = require('path');
|
||||
@@ -30,7 +30,8 @@ interface RendererProcessQuitReply {
|
||||
}
|
||||
|
||||
interface PluginWindows {
|
||||
[key: string]: BrowserWindow;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type SecondaryWindowId = string;
|
||||
@@ -47,6 +48,7 @@ export interface Options {
|
||||
}
|
||||
|
||||
export default class ElectronAppWrapper {
|
||||
private logger_: Logger = null;
|
||||
private electronApp_: App;
|
||||
private env_: string;
|
||||
private isDebugMode_: boolean;
|
||||
@@ -59,8 +61,8 @@ export default class ElectronAppWrapper {
|
||||
private secondaryWindows_: Map<SecondaryWindowId, SecondaryWindowData> = new Map();
|
||||
|
||||
private willQuitApp_ = false;
|
||||
private enableUnresponsiveCheck_ = true;
|
||||
private tray_: Tray = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private tray_: any = null;
|
||||
private buildDir_: string = null;
|
||||
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
|
||||
|
||||
@@ -68,15 +70,13 @@ export default class ElectronAppWrapper {
|
||||
private updaterService_: AutoUpdaterService = null;
|
||||
private customProtocolHandlers_: CustomProtocolHandlers|null = null;
|
||||
private updatePollInterval_: ReturnType<typeof setTimeout>|null = null;
|
||||
private joplinSession_: Session|null = null;
|
||||
|
||||
private profileLocker_: FileLocker|null = null;
|
||||
private ipcServer_: IpcServer|null = null;
|
||||
private ipcStartPort_ = 2658;
|
||||
|
||||
private mainProcessLoggerFilePath_: string;
|
||||
private ipcLogger_: LoggerWrapper;
|
||||
private appLogger_: LoggerWrapper;
|
||||
private ipcLogger_: Logger;
|
||||
private ipcLoggerFilePath_: string;
|
||||
|
||||
public constructor(electronApp: App, { env, profilePath, isDebugMode, initialCallbackUrl, isEndToEndTesting }: Options) {
|
||||
this.electronApp_ = electronApp;
|
||||
@@ -88,20 +88,28 @@ export default class ElectronAppWrapper {
|
||||
|
||||
this.profileLocker_ = new FileLocker(`${this.profilePath_}/lock`);
|
||||
|
||||
const mainProcessLogger = new Logger();
|
||||
this.mainProcessLoggerFilePath_ = `${profilePath}/log-main-process.txt`;
|
||||
mainProcessLogger.addTarget(TargetType.File, {
|
||||
path: this.mainProcessLoggerFilePath_,
|
||||
// Note: in certain contexts `this.logger_` doesn't seem to be available, especially for IPC
|
||||
// calls, either because it hasn't been set or other issue. So we set one here specifically
|
||||
// for this.
|
||||
this.ipcLogger_ = new Logger();
|
||||
this.ipcLoggerFilePath_ = `${profilePath}/log-cross-app-ipc.txt`;
|
||||
this.ipcLogger_.addTarget(TargetType.File, {
|
||||
path: this.ipcLoggerFilePath_,
|
||||
});
|
||||
|
||||
this.ipcLogger_ = Logger.create('IPC', mainProcessLogger);
|
||||
this.appLogger_ = Logger.create('App', mainProcessLogger);
|
||||
}
|
||||
|
||||
public electronApp() {
|
||||
return this.electronApp_;
|
||||
}
|
||||
|
||||
public setLogger(v: Logger) {
|
||||
this.logger_ = v;
|
||||
}
|
||||
|
||||
public logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
public mainWindow() {
|
||||
return this.win_;
|
||||
}
|
||||
@@ -114,8 +122,8 @@ export default class ElectronAppWrapper {
|
||||
return !!this.ipcServer_;
|
||||
}
|
||||
|
||||
public mainProcessLogFilePath() {
|
||||
return this.mainProcessLoggerFilePath_;
|
||||
public ipcLoggerFilePath() {
|
||||
return this.ipcLoggerFilePath_;
|
||||
}
|
||||
|
||||
public windowById(joplinId: string) {
|
||||
@@ -203,46 +211,13 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
private createJoplinSession_() {
|
||||
const sessionPath = path.join(this.profilePath_, 'internal');
|
||||
const joplinSession = electronSession.fromPath(sessionPath, { cache: false });
|
||||
|
||||
// One-time migration: copy existing dictionary words from the old Electron userData location into the new session.
|
||||
const migrationFlagPath = path.join(this.profilePath_, 'spell-checker-migration-done');
|
||||
if (!fs.existsSync(migrationFlagPath)) {
|
||||
try {
|
||||
const wordsToMigrate = new Set<string>();
|
||||
|
||||
const oldElectronDictPath = path.join(this.electronApp_.getPath('userData'), 'Custom Dictionary.txt');
|
||||
if (fs.existsSync(oldElectronDictPath)) {
|
||||
const content = fs.readFileSync(oldElectronDictPath, 'utf8');
|
||||
const words = content.split('\n')
|
||||
.map((w: string) => w.trim())
|
||||
.filter((w: string) => w.length > 0 && !/^checksum_v1\s*=/.test(w));
|
||||
|
||||
for (const word of words) {
|
||||
wordsToMigrate.add(word);
|
||||
}
|
||||
}
|
||||
|
||||
for (const word of wordsToMigrate) {
|
||||
joplinSession.addWordToSpellCheckerDictionary(word);
|
||||
}
|
||||
|
||||
fs.writeFileSync(migrationFlagPath, '', 'utf8');
|
||||
} catch (error) {
|
||||
console.warn('Failed to migrate spell-check dictionary:', error);
|
||||
}
|
||||
}
|
||||
return joplinSession;
|
||||
}
|
||||
|
||||
public createWindow() {
|
||||
// Set to true to view errors if the application does not start
|
||||
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
|
||||
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const stateOptions: any = {
|
||||
defaultWidth: Math.round(0.8 * screen.getPrimaryDisplay().workArea.width),
|
||||
@@ -268,7 +243,6 @@ export default class ElectronAppWrapper {
|
||||
// this needs to be a non-transparent color:
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#333' : '#fff',
|
||||
webPreferences: {
|
||||
session: this.joplinSession_,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: true,
|
||||
@@ -308,8 +282,6 @@ export default class ElectronAppWrapper {
|
||||
let unresponsiveTimeout: ReturnType<typeof setTimeout>|null = null;
|
||||
|
||||
this.win_.webContents.on('unresponsive', () => {
|
||||
if (!this.enableUnresponsiveCheck_) return;
|
||||
|
||||
// Don't show the "unresponsive" dialog immediately -- the "unresponsive" event
|
||||
// can be fired when showing a dialog or modal (e.g. the update dialog).
|
||||
//
|
||||
@@ -380,7 +352,7 @@ export default class ElectronAppWrapper {
|
||||
} catch (error) {
|
||||
// This will throw an exception "Object has been destroyed" if the app is closed
|
||||
// in less that the timeout interval. It can be ignored.
|
||||
this.appLogger_.warn('Error opening dev tools', error);
|
||||
console.warn('Error opening dev tools', error);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
@@ -433,6 +405,15 @@ export default class ElectronAppWrapper {
|
||||
};
|
||||
addWindowEventHandlers(this.win_.webContents);
|
||||
|
||||
// BrowserWindow 'focus' fires when the OS gives focus to the application window
|
||||
// (i.e. coming from another app or from the taskbar), not on intra-app focus switches.
|
||||
// We use a dedicated IPC channel so the renderer can trigger an immediate sync on
|
||||
// OS-level focus gain without conflating it with the 'window-focused' channel that
|
||||
// handles Joplin-internal window routing.
|
||||
this.win_.on('focus', () => {
|
||||
this.win_?.webContents.send('main-window-focused');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
this.win_.on('close', (event: any) => {
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
@@ -442,15 +423,12 @@ export default class ElectronAppWrapper {
|
||||
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
|
||||
// case the app must be explicitly closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
|
||||
|
||||
this.appLogger_.info('[appClose] Window close event - willQuitApp_:', this.willQuitApp_, 'rendererProcessQuitReply_:', this.rendererProcessQuitReply_, 'secondaryWindows:', this.secondaryWindows_.size, 'trayShown:', this.trayShown());
|
||||
|
||||
let isGoingToExit = false;
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (this.willQuitApp_) {
|
||||
isGoingToExit = true;
|
||||
} else {
|
||||
this.appLogger_.info('[appClose] macOS: willQuitApp_ is false, hiding window instead of closing');
|
||||
event.preventDefault();
|
||||
|
||||
const w = this.win_;
|
||||
@@ -474,27 +452,21 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
this.appLogger_.info('[appClose] isGoingToExit:', isGoingToExit);
|
||||
|
||||
if (isGoingToExit) {
|
||||
if (!this.rendererProcessQuitReply_) {
|
||||
// If we haven't notified the renderer process yet, do it now
|
||||
// so that it can tell us if we can really close the app or not.
|
||||
// Search for "appClose" event for closing logic on renderer side.
|
||||
this.appLogger_.info('[appClose] Sending appClose to renderer, waiting for reply...');
|
||||
event.preventDefault();
|
||||
if (this.win_) this.win_.webContents.send('appClose');
|
||||
} else {
|
||||
// If the renderer process has responded, check if we can close or not
|
||||
this.appLogger_.info('[appClose] Got renderer reply - canClose:', this.rendererProcessQuitReply_.canClose);
|
||||
if (this.rendererProcessQuitReply_.canClose) {
|
||||
// Really quit the app
|
||||
this.appLogger_.info('[appClose] Closing app now');
|
||||
this.rendererProcessQuitReply_ = null;
|
||||
this.win_ = null;
|
||||
} else {
|
||||
// Wait for renderer to finish task
|
||||
this.appLogger_.info('[appClose] Renderer says cannot close yet, waiting...');
|
||||
event.preventDefault();
|
||||
this.rendererProcessQuitReply_ = null;
|
||||
}
|
||||
@@ -510,31 +482,8 @@ export default class ElectronAppWrapper {
|
||||
// Match the main window's zoom:
|
||||
window.webContents.setZoomFactor(this.mainWindow().webContents.getZoomFactor());
|
||||
|
||||
window.once('close', (event) => {
|
||||
// Check both: BrowserWindow and webContents can be destroyed independently
|
||||
if (this.win_ && !this.win_.isDestroyed() && !this.win_.webContents.isDestroyed()) {
|
||||
this.win_.webContents.send('secondary-window-closing', windowId);
|
||||
}
|
||||
if (this.secondaryWindows_.has(windowId)) {
|
||||
this.secondaryWindows_.delete(windowId);
|
||||
|
||||
// Avoid closing a destroyed window. Closing a destroyed window results in the following error:
|
||||
// Error: Render frame was disposed before WebFrameMain could be accessed
|
||||
const stillOpen = !window.isDestroyed();
|
||||
if (stillOpen) {
|
||||
event.preventDefault();
|
||||
|
||||
// As of March 2026, Electron crashes with "Assertion failed: (Environment::GetCurrent(isolate)) == (env)" if the native 'close'
|
||||
// event is allowed to close a secondary window. As a workaround, briefly hide the window and .close() it later.
|
||||
// See https://github.com/laurent22/joplin/issues/14628.
|
||||
window.hide();
|
||||
setTimeout(() => {
|
||||
if (!window.isDestroyed()) {
|
||||
window.close();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
window.once('close', () => {
|
||||
this.secondaryWindows_.delete(windowId);
|
||||
|
||||
const allSecondaryWindowsClosed = this.secondaryWindows_.size === 0;
|
||||
const mainWindowVisuallyClosed = this.mainWindowHidden_;
|
||||
@@ -582,8 +531,8 @@ export default class ElectronAppWrapper {
|
||||
// sends a message. In which case, the above code would try to
|
||||
// access a destroyed webview.
|
||||
// https://github.com/laurent22/joplin/issues/4570
|
||||
this.appLogger_.error('Could not process plugin message:', message);
|
||||
this.appLogger_.error(error);
|
||||
console.error('Could not process plugin message:', message);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -608,7 +557,8 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
public registerPluginWindow(pluginId: string, window: BrowserWindow) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public registerPluginWindow(pluginId: string, window: any) {
|
||||
this.pluginWindows_[pluginId] = window;
|
||||
}
|
||||
|
||||
@@ -637,7 +587,6 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
|
||||
public quit() {
|
||||
this.appLogger_.info('[appClose] quit() called');
|
||||
this.onExit();
|
||||
this.electronApp_.quit();
|
||||
}
|
||||
@@ -646,7 +595,6 @@ export default class ElectronAppWrapper {
|
||||
dispatch: (action: { type: string; [key: string]: unknown })=> void,
|
||||
syncPending: boolean,
|
||||
) {
|
||||
this.appLogger_.info('[appClose] quitWithSyncCheck() called - syncPending:', syncPending);
|
||||
if (syncPending) {
|
||||
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
|
||||
} else {
|
||||
@@ -696,7 +644,8 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
|
||||
// Note: this must be called only after the "ready" event of the app has been dispatched
|
||||
public createTray(contextMenu: Menu) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public createTray(contextMenu: any) {
|
||||
try {
|
||||
this.tray_ = new Tray(`${this.buildDir()}/icons/${this.trayIconFilename_()}`);
|
||||
this.tray_.setToolTip(this.electronApp_.name);
|
||||
@@ -704,7 +653,7 @@ export default class ElectronAppWrapper {
|
||||
|
||||
this.tray_.on('click', () => {
|
||||
if (!this.mainWindow()) {
|
||||
this.appLogger_.warn('The window object was not available during the click event from tray icon');
|
||||
console.warn('The window object was not available during the click event from tray icon');
|
||||
return;
|
||||
}
|
||||
if (!this.mainWindow().isVisible()) {
|
||||
@@ -714,7 +663,7 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.appLogger_.error('Cannot create tray', error);
|
||||
console.error('Cannot create tray', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,7 +810,7 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
|
||||
this.quit();
|
||||
if (this.env() === 'dev') this.appLogger_.warn(`Closing the application because another instance is already running, or the previous instance was force-quit within the last ${Math.round(this.profileLocker_.options.interval / Second)} seconds.`);
|
||||
if (this.env() === 'dev') console.warn(`Closing the application because another instance is already running, or the previous instance was force-quit within the last ${Math.round(this.profileLocker_.options.interval / Second)} seconds.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -899,10 +848,6 @@ export default class ElectronAppWrapper {
|
||||
return this.customProtocolHandlers_.pluginContent;
|
||||
}
|
||||
|
||||
public setEnableUnresponsiveCheck(enabled: boolean) {
|
||||
this.enableUnresponsiveCheck_ = enabled;
|
||||
}
|
||||
|
||||
private async fixLinuxAccessibility_() {
|
||||
if (this.electronApp().accessibilitySupportEnabled) return;
|
||||
|
||||
@@ -913,7 +858,8 @@ export default class ElectronAppWrapper {
|
||||
return matchingProcesses.trim().length > 0;
|
||||
} catch (error) {
|
||||
if (error.stderr || error.exitCode !== 1) {
|
||||
this.appLogger_.error('Failed to check for and enable accessibility support:', error.stderr);
|
||||
// eslint-disable-next-line no-console -- The main logger is not available at this point.
|
||||
console.error('Failed to check for and enable accessibility support:', error.stderr);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -923,7 +869,8 @@ export default class ElectronAppWrapper {
|
||||
// Work around https://issues.chromium.org/issues/431257156 by force-enabling accessibility
|
||||
// when Orca (a screen reader) is running:
|
||||
if (await isOrcaRunning()) {
|
||||
this.appLogger_.info('Linux accessibility: Enabling full accessibility support.');
|
||||
// eslint-disable-next-line no-console -- The main logger is not available at this point.
|
||||
console.log('Linux accessibility: Enabling full accessibility support.');
|
||||
this.electronApp().setAccessibilitySupportEnabled(true);
|
||||
}
|
||||
}
|
||||
@@ -938,19 +885,14 @@ export default class ElectronAppWrapper {
|
||||
|
||||
await this.fixLinuxAccessibility_();
|
||||
|
||||
// Session must be created before handleCustomProtocols() so both use the same object.
|
||||
this.joplinSession_ = this.createJoplinSession_();
|
||||
this.customProtocolHandlers_ = handleCustomProtocols(this.joplinSession_);
|
||||
this.customProtocolHandlers_ = handleCustomProtocols();
|
||||
this.createWindow();
|
||||
|
||||
this.electronApp_.on('before-quit', () => {
|
||||
this.appLogger_.info('[appClose] before-quit event fired, setting willQuitApp_ = true');
|
||||
this.willQuitApp_ = true;
|
||||
bridge().unregisterGlobalHotkey();
|
||||
});
|
||||
|
||||
this.electronApp_.on('window-all-closed', () => {
|
||||
this.appLogger_.info('[appClose] window-all-closed event fired');
|
||||
this.quit();
|
||||
});
|
||||
|
||||
@@ -963,6 +905,11 @@ export default class ElectronAppWrapper {
|
||||
event.preventDefault();
|
||||
void this.openCallbackUrl(url);
|
||||
});
|
||||
|
||||
// When the OS wakes from sleep, notify the renderer so it can trigger an immediate sync.
|
||||
powerMonitor.on('resume', () => {
|
||||
this.win_?.webContents.send('system-resumed');
|
||||
});
|
||||
}
|
||||
|
||||
public async openCallbackUrl(url: string) {
|
||||
|
||||
@@ -11,7 +11,8 @@ const logger = Logger.create('app.reducer');
|
||||
export interface AppStateRoute {
|
||||
type: string;
|
||||
routeName: string;
|
||||
props: Record<string, unknown>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
props: any;
|
||||
}
|
||||
|
||||
export enum AppStateDialogName {
|
||||
@@ -21,7 +22,8 @@ export enum AppStateDialogName {
|
||||
|
||||
export interface AppStateDialog {
|
||||
name: AppStateDialogName;
|
||||
props: Record<string, unknown>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
props: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface NoteIdToScrollPercent {
|
||||
|
||||
@@ -78,7 +78,8 @@ type StartupTask = { label: string; task: ()=> void|Promise<void> };
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
private checkAllPluginStartedIID_: ReturnType<typeof setInterval> = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private checkAllPluginStartedIID_: any = null;
|
||||
private initPluginServiceDone_ = false;
|
||||
private ocrService_: OcrService;
|
||||
private protocolHandler_: CustomContentProtocolHandler;
|
||||
@@ -150,10 +151,6 @@ class Application extends BaseApplication {
|
||||
bridge().extraAllowedOpenExtensions = Setting.value('linking.extraAllowedExtensions');
|
||||
}
|
||||
|
||||
if ((action.type === 'SETTING_UPDATE_ONE' && action.key === 'globalHotkey') || action.type === 'SETTING_UPDATE_ALL') {
|
||||
bridge().updateGlobalHotkey(Setting.value('globalHotkey'));
|
||||
}
|
||||
|
||||
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
|
||||
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
|
||||
}
|
||||
@@ -737,9 +734,22 @@ class Application extends BaseApplication {
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('secondary-window-closing', (_event, windowId: string) => {
|
||||
this.dispatch({ type: 'WINDOW_CLOSE', windowId });
|
||||
});
|
||||
// Trigger an immediate sync when the main window gains OS-level focus (i.e. the user
|
||||
// switches back to Joplin from another application) or when the system wakes from sleep.
|
||||
// A 30-second cool-down prevents duplicate syncs during rapid focus-in/focus-out cycles.
|
||||
const minResumeSyncIntervalMs = 30_000;
|
||||
let lastFocusSyncTime = 0;
|
||||
|
||||
const scheduleResumeSync = () => {
|
||||
const now = Date.now();
|
||||
if (now - lastFocusSyncTime > minResumeSyncIntervalMs) {
|
||||
lastFocusSyncTime = now;
|
||||
void reg.scheduleSync(0);
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.on('main-window-focused', scheduleResumeSync);
|
||||
ipcRenderer.on('system-resumed', scheduleResumeSync);
|
||||
});
|
||||
|
||||
addTask('app/initPluginService', () => this.initPluginService());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ElectronAppWrapper from './ElectronAppWrapper';
|
||||
import shim, { MessageBoxType } from '@joplin/lib/shim';
|
||||
import { _, setLocale } from '@joplin/lib/locale';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage, Menu, MenuItemConstructorOptions, MenuItem, BrowserWindowConstructorOptions, FileFilter, SaveDialogOptions, globalShortcut } from 'electron';
|
||||
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage, Menu, MenuItemConstructorOptions, MenuItem } from 'electron';
|
||||
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
|
||||
import { fileUriToPath } from '@joplin/utils/url';
|
||||
import { urlDecode } from '@joplin/lib/string-utils';
|
||||
@@ -25,7 +25,8 @@ interface OpenDialogOptions {
|
||||
properties?: string[];
|
||||
defaultPath?: string;
|
||||
createDirectory?: boolean;
|
||||
filters?: FileFilter[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
filters?: any[];
|
||||
}
|
||||
|
||||
type OnAllowedExtensionsChange = (newExtensions: string[])=> void;
|
||||
@@ -46,7 +47,6 @@ export class Bridge {
|
||||
|
||||
private extraAllowedExtensions_: string[] = [];
|
||||
private onAllowedExtensionsChangeListener_: OnAllowedExtensionsChange = ()=>{};
|
||||
private registeredGlobalHotkey_ = '';
|
||||
|
||||
public constructor(electronWrapper: ElectronAppWrapper, appId: string, appName: string, rootProfileDir: string, autoUploadCrashDumps: boolean, altInstanceId: string) {
|
||||
this.electronWrapper_ = electronWrapper;
|
||||
@@ -208,55 +208,8 @@ export class Bridge {
|
||||
this.onAllowedExtensionsChangeListener_ = listener;
|
||||
}
|
||||
|
||||
public updateGlobalHotkey(accelerator: string) {
|
||||
// Skip if the accelerator hasn't changed
|
||||
if (accelerator === this.registeredGlobalHotkey_) return;
|
||||
|
||||
// Unregister the previous shortcut (only Joplin's own)
|
||||
this.unregisterGlobalHotkey();
|
||||
|
||||
if (!accelerator) return;
|
||||
|
||||
try {
|
||||
const registered = globalShortcut.register(accelerator, () => {
|
||||
const win = this.mainWindow();
|
||||
if (!win) return;
|
||||
|
||||
if (win.isVisible() && win.isFocused()) {
|
||||
win.hide();
|
||||
} else {
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.show();
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
win.focus();
|
||||
}
|
||||
});
|
||||
|
||||
if (registered) {
|
||||
this.registeredGlobalHotkey_ = accelerator;
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Bridge: Failed to register global shortcut: ${accelerator}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Bridge: Error registering global shortcut "${accelerator}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public unregisterGlobalHotkey() {
|
||||
if (this.registeredGlobalHotkey_) {
|
||||
try {
|
||||
globalShortcut.unregister(this.registeredGlobalHotkey_);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Bridge: Error removing global shortcut:', error);
|
||||
}
|
||||
this.registeredGlobalHotkey_ = '';
|
||||
}
|
||||
}
|
||||
|
||||
public async captureException(error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async captureException(error: any) {
|
||||
Sentry.captureException(error);
|
||||
// We wait to give the "beforeSend" event handler time to process the crash dump and write
|
||||
// it to file.
|
||||
@@ -382,7 +335,8 @@ export class Bridge {
|
||||
return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath));
|
||||
}
|
||||
|
||||
public newBrowserWindow(options: BrowserWindowConstructorOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public newBrowserWindow(options: any) {
|
||||
return new BrowserWindow(options);
|
||||
}
|
||||
|
||||
@@ -399,7 +353,8 @@ export class Bridge {
|
||||
return this.activeWindow().webContents.closeDevTools();
|
||||
}
|
||||
|
||||
public async showSaveDialog(options: SaveDialogOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async showSaveDialog(options: any) {
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file;
|
||||
const { filePath } = await dialog.showSaveDialog(this.activeWindow(), options);
|
||||
@@ -426,7 +381,8 @@ export class Bridge {
|
||||
}
|
||||
|
||||
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead
|
||||
private showMessageBox_(window: BrowserWindow, options: MessageDialogOptions): number {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private showMessageBox_(window: any, options: MessageDialogOptions): number {
|
||||
if (!window) window = this.activeWindow();
|
||||
return dialog.showMessageBoxSync(window, { message: '', ...options });
|
||||
}
|
||||
@@ -472,7 +428,8 @@ export class Bridge {
|
||||
return result;
|
||||
}
|
||||
|
||||
public showInfoMessageBox(message: string, options: MessageDialogOptions = {}) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public showInfoMessageBox(message: string, options: any = {}) {
|
||||
const result = this.showMessageBox_(this.activeWindow(), { type: 'info',
|
||||
message: message,
|
||||
buttons: [_('OK')], ...options });
|
||||
@@ -483,10 +440,6 @@ export class Bridge {
|
||||
setLocale(locale);
|
||||
}
|
||||
|
||||
public setEnableUnresponsiveCheck(enabled: boolean) {
|
||||
this.electronWrapper_.setEnableUnresponsiveCheck(enabled);
|
||||
}
|
||||
|
||||
public get Menu() {
|
||||
return Menu;
|
||||
}
|
||||
@@ -606,7 +559,7 @@ export class Bridge {
|
||||
});
|
||||
|
||||
if (buttonIndex === 1) {
|
||||
void this.openItem(this.electronApp().mainProcessLogFilePath());
|
||||
void this.openItem(this.electronApp().ipcLoggerFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ function truncateText(text: string, length: number) {
|
||||
}
|
||||
|
||||
async function getSkippedVersions(): Promise<string[]> {
|
||||
const r = await KvStore.instance().value('updateCheck::skippedVersions');
|
||||
return r && typeof r === 'string' ? JSON.parse(r) : [];
|
||||
const r = await KvStore.instance().value<string>('updateCheck::skippedVersions');
|
||||
return r ? JSON.parse(r) : [];
|
||||
}
|
||||
|
||||
async function isSkippedVersion(v: string): Promise<boolean> {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import GlobalHotkeyInput from './GlobalHotkeyInput';
|
||||
|
||||
describe('GlobalHotkeyInput', () => {
|
||||
test('should render ShortcutRecorder with Save and Restore buttons', () => {
|
||||
const onChange = jest.fn();
|
||||
render(<GlobalHotkeyInput value="CommandOrControl+Shift+J" themeId={1} onChange={onChange} />);
|
||||
|
||||
// ShortcutRecorder is always visible with its built-in buttons
|
||||
expect(screen.getByText('Save')).toBeTruthy();
|
||||
expect(screen.getByText('Restore')).toBeTruthy();
|
||||
expect(screen.getByText('Cancel')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should clear value when Restore is clicked', () => {
|
||||
const onChange = jest.fn();
|
||||
render(<GlobalHotkeyInput value="CommandOrControl+Shift+J" themeId={1} onChange={onChange} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Restore'));
|
||||
expect(onChange).toHaveBeenCalledWith({ value: '' });
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { ShortcutRecorder } from '../../KeymapConfig/ShortcutRecorder';
|
||||
|
||||
interface OnChangeEvent {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
themeId: number;
|
||||
onChange: (event: OnChangeEvent)=> void;
|
||||
}
|
||||
|
||||
// A thin wrapper around ShortcutRecorder for the global hotkey setting.
|
||||
// Reuses ShortcutRecorder directly instead of maintaining a separate display mode.
|
||||
export default function GlobalHotkeyInput(props: Props) {
|
||||
const value = props.value || '';
|
||||
|
||||
const onSave = useCallback((event: { commandName: string; accelerator: string }) => {
|
||||
// Normalize platform-specific modifiers to CommandOrControl for
|
||||
// consistent cross-platform storage.
|
||||
const accelerator = event.accelerator
|
||||
.replace(/\bCmd\b/, 'CommandOrControl')
|
||||
.replace(/\bCtrl\b/, 'CommandOrControl');
|
||||
props.onChange({ value: accelerator });
|
||||
}, [props.onChange]);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
props.onChange({ value: '' });
|
||||
}, [props.onChange]);
|
||||
|
||||
// No-op: global hotkeys don't have a separate editing mode to cancel out of.
|
||||
const onCancel = useCallback(() => {}, []);
|
||||
|
||||
// No-op: ShortcutRecorder validates against the keymap (command
|
||||
// conflicts), which doesn't apply to global hotkeys.
|
||||
const onError = useCallback((_event: { recorderError: Error }) => {}, []);
|
||||
|
||||
return (
|
||||
<ShortcutRecorder
|
||||
onSave={onSave}
|
||||
onReset={onReset}
|
||||
onCancel={onCancel}
|
||||
onError={onError}
|
||||
initialAccelerator={value}
|
||||
commandName="globalHotkey"
|
||||
themeId={props.themeId}
|
||||
skipKeymapValidation
|
||||
autoFocus={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { themeStyle } from '@joplin/lib/theme';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useId } from 'react';
|
||||
import control_PluginsStates from './plugins/PluginsStates';
|
||||
import control_GlobalHotkeyInput from './GlobalHotkeyInput';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Button, { ButtonLevel, ButtonSize } from '../../Button/Button';
|
||||
@@ -12,10 +11,8 @@ import * as pathUtils from '@joplin/lib/path-utils';
|
||||
import SettingLabel from './SettingLabel';
|
||||
import SettingDescription from './SettingDescription';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each control component has different prop types
|
||||
const settingKeyToControl: Record<string, React.FC<any>> = {
|
||||
const settingKeyToControl: Record<string, typeof control_PluginsStates> = {
|
||||
'plugins.states': control_PluginsStates,
|
||||
'globalHotkey': control_GlobalHotkeyInput,
|
||||
};
|
||||
|
||||
export interface UpdateSettingValueEvent {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Root = styled.h1<{ justifyContent?: string }>`
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const Root = styled.h1<any>`
|
||||
display: flex;
|
||||
justify-content: ${props => props.justifyContent ? props.justifyContent : 'center'};
|
||||
font-family: ${props => props.theme.fontFamily};
|
||||
|
||||
@@ -8,7 +8,7 @@ const { themeStyle } = require('@joplin/lib/theme');
|
||||
const Shared = require('@joplin/lib/components/shared/dropbox-login-shared');
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
themeId: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -63,7 +63,8 @@ class DropboxLoginScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: { settings: { theme: number } }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const mapStateToProps = (state: any) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
|
||||
@@ -47,13 +47,10 @@ export default function(props: Props) {
|
||||
}, [props.dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!titleInputRef.current) return;
|
||||
focus('Dialog::titleInputRef', titleInputRef.current);
|
||||
|
||||
setTimeout(() => {
|
||||
if (titleInputRef.current) {
|
||||
titleInputRef.current.select();
|
||||
}
|
||||
titleInputRef.current.select();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvance
|
||||
import MacOSMissingPasswordHelpLink from '../ConfigScreen/controls/MissingPasswordHelpLink';
|
||||
import { Dispatch } from 'redux';
|
||||
import { shouldCancelPendingEnableAfterMasterPasswordDialog, shouldOpenMasterPasswordDialogForEnable, shouldResumeEnableAfterMasterPasswordDialog } from './enableFlow';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import DialogButtonRow from '../DialogButtonRow';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import PasswordInput from '../PasswordInput/PasswordInput';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -43,10 +39,6 @@ interface Props {
|
||||
export const EncryptionConfigScreen = (props: Props) => {
|
||||
const { inputPasswords, onInputPasswordChange } = useInputPasswords(props.passwords);
|
||||
const [pendingEnableEncryption, setPendingEnableEncryption] = useState(false);
|
||||
const [enableEncryptionPromptVisible, setEnableEncryptionPromptVisible] = useState(false);
|
||||
const [enableEncryptionPassword, setEnableEncryptionPassword] = useState('');
|
||||
const promptPromiseRef = useRef<(password: string | null)=> void>(null);
|
||||
|
||||
const wasMasterPasswordDialogOpen = useRef(props.masterPasswordDialogOpen);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
@@ -243,7 +235,7 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
const newEnabled = !isEnabled;
|
||||
const masterKey = getDefaultMasterKey();
|
||||
const hasMasterPassword = !!props.masterPassword;
|
||||
let newPassword: string | null = '';
|
||||
let newPassword = '';
|
||||
|
||||
if (isEnabled) {
|
||||
const answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
@@ -261,14 +253,8 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the custom React Dialog to resolve
|
||||
setEnableEncryptionPassword('');
|
||||
setEnableEncryptionPromptVisible(true);
|
||||
newPassword = await new Promise<string | null>((resolve) => {
|
||||
promptPromiseRef.current = resolve;
|
||||
});
|
||||
|
||||
if (newPassword === null) return; // User cancelled
|
||||
const msg = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword);
|
||||
newPassword = await dialogs.prompt(msg.join('\n\n'), '', '', { type: 'password' });
|
||||
}
|
||||
|
||||
if (hasMasterPassword && newEnabled) {
|
||||
@@ -285,63 +271,6 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
}
|
||||
}, [props.dispatch, props.masterPassword, props.masterPasswordDialogOpen]);
|
||||
|
||||
const renderEnableEncryptionDialog = () => {
|
||||
if (!enableEncryptionPromptVisible) return null;
|
||||
|
||||
const masterKey = getDefaultMasterKey();
|
||||
const hasMasterPassword = !!props.masterPassword;
|
||||
|
||||
const msg = enableEncryptionConfirmationMessages(masterKey, hasMasterPassword);
|
||||
const messageComps = msg.map((m, index) => <p key={index} style={theme.textStyle}>{m}</p>);
|
||||
|
||||
const onClose = () => {
|
||||
setEnableEncryptionPromptVisible(false);
|
||||
if (promptPromiseRef.current) promptPromiseRef.current(null);
|
||||
};
|
||||
|
||||
const onDialogButtonRowClick = (event: { buttonName: string }) => {
|
||||
if (event.buttonName === 'cancel') {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (event.buttonName === 'ok') {
|
||||
setEnableEncryptionPromptVisible(false);
|
||||
if (promptPromiseRef.current) promptPromiseRef.current(enableEncryptionPassword);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Required because PasswordInput's ChangeEventHandler type is incorrect
|
||||
const onPasswordInputChange = (event: any) => {
|
||||
setEnableEncryptionPassword(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog onCancel={onClose} className="enable-encryption-dialog">
|
||||
<div className="dialog-root">
|
||||
<DialogTitle title={_('Enable encryption')}/>
|
||||
<div className="dialog-content">
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{messageComps}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ ...theme.textStyle, marginBottom: 5, display: 'block' }} htmlFor="enable-encryption-password">{_('Password:')}</label>
|
||||
<PasswordInput
|
||||
inputId="enable-encryption-password"
|
||||
value={enableEncryptionPassword}
|
||||
onChange={onPasswordInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtonRow
|
||||
themeId={props.themeId}
|
||||
onClick={onDialogButtonRowClick}
|
||||
okButtonDisabled={!enableEncryptionPassword}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEncryptionSection = () => {
|
||||
const decryptedItemsInfo = <p>{decryptedStatText(stats)}</p>;
|
||||
const toggleButton = (
|
||||
@@ -522,7 +451,6 @@ export const EncryptionConfigScreen = (props: Props) => {
|
||||
{renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false)}
|
||||
{renderNonExistingMasterKeysSection()}
|
||||
{renderAdvancedSection()}
|
||||
{renderEnableEncryptionDialog()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,7 +31,8 @@ interface State {
|
||||
|
||||
interface Props {
|
||||
message?: string;
|
||||
children: React.ReactNode;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
children: any;
|
||||
}
|
||||
|
||||
interface BannerProps {
|
||||
|
||||
@@ -6,12 +6,14 @@ import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
tip: string;
|
||||
onClick: ()=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClick: Function;
|
||||
themeId: number;
|
||||
style?: React.CSSProperties;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
|
||||
'aria-controls'?: string;
|
||||
'aria-expanded'?: boolean;
|
||||
'aria-expanded'?: string;
|
||||
}
|
||||
|
||||
class HelpButtonComponent extends React.Component<Props> {
|
||||
@@ -29,7 +31,8 @@ class HelpButtonComponent extends React.Component<Props> {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = { ...this.props.style, color: theme.color, textDecoration: 'none' };
|
||||
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
|
||||
const extraProps: Record<string, string> = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const extraProps: any = {};
|
||||
if (this.props.tip) {
|
||||
extraProps['data-tip'] = this.props.tip;
|
||||
extraProps['aria-description'] = this.props.tip;
|
||||
|
||||
@@ -3,9 +3,11 @@ import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
style?: React.CSSProperties;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
iconName: string;
|
||||
onClick: ()=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClick: Function;
|
||||
}
|
||||
|
||||
class IconButton extends React.Component<Props> {
|
||||
@@ -18,7 +20,7 @@ class IconButton extends React.Component<Props> {
|
||||
};
|
||||
const icon = <i style={iconStyle} className={`fas ${this.props.iconName}`}></i>;
|
||||
|
||||
const rootStyle: React.CSSProperties = {
|
||||
const rootStyle = {
|
||||
display: 'flex',
|
||||
textDecoration: 'none',
|
||||
padding: 10,
|
||||
|
||||
@@ -15,14 +15,9 @@ export interface ShortcutRecorderProps {
|
||||
initialAccelerator: string;
|
||||
commandName: string;
|
||||
themeId: number;
|
||||
// When true, skip keymap conflict validation (useful for global hotkeys
|
||||
// that aren't part of the internal command keymap).
|
||||
skipKeymapValidation?: boolean;
|
||||
// Controls whether the input auto-focuses on mount. Defaults to true.
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAccelerator, commandName, themeId, skipKeymapValidation, autoFocus = true }: ShortcutRecorderProps) => {
|
||||
export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAccelerator, commandName, themeId }: ShortcutRecorderProps) => {
|
||||
const styles = styles_(themeId);
|
||||
|
||||
const [accelerator, setAccelerator] = useState(initialAccelerator);
|
||||
@@ -34,9 +29,7 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
|
||||
// Otherwise performing a save means that it's going to be disabled
|
||||
if (accelerator) {
|
||||
keymapService.validateAccelerator(accelerator);
|
||||
if (!skipKeymapValidation) {
|
||||
keymapService.validateKeymap({ accelerator, command: commandName });
|
||||
}
|
||||
keymapService.validateKeymap({ accelerator, command: commandName });
|
||||
}
|
||||
|
||||
// Discard previous errors
|
||||
@@ -93,7 +86,7 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
|
||||
|
||||
onKeyDown={handleKeyDown}
|
||||
readOnly
|
||||
autoFocus={autoFocus}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<button style={styles.inlineButton} disabled={!saveAllowed} onClick={() => onSave({ commandName, accelerator })}>
|
||||
|
||||
@@ -45,9 +45,6 @@ import PluginNotification from './PluginNotification/PluginNotification';
|
||||
import { Toast } from '@joplin/lib/services/plugins/api/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import QuitSyncDialog from './QuitSyncDialog';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('MainScreen');
|
||||
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
@@ -280,12 +277,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
// If a note is being saved, we wait till it is saved and then call
|
||||
// "appCloseReply" again.
|
||||
ipcRenderer.on('appClose', async () => {
|
||||
logger.info('[appClose] Received appClose event - hasNotesBeingSaved:', this.props.hasNotesBeingSaved);
|
||||
if (this.waitForNotesSavedIID_) shim.clearInterval(this.waitForNotesSavedIID_);
|
||||
this.waitForNotesSavedIID_ = null;
|
||||
|
||||
const sendCanClose = async (canClose: boolean) => {
|
||||
logger.info('[appClose] Sending appCloseReply - canClose:', canClose);
|
||||
if (canClose) {
|
||||
Setting.setValue('wasClosedSuccessfully', true);
|
||||
await Setting.saveAll();
|
||||
@@ -296,10 +291,8 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
await sendCanClose(!this.props.hasNotesBeingSaved);
|
||||
|
||||
if (this.props.hasNotesBeingSaved) {
|
||||
logger.info('[appClose] Notes are being saved, waiting...');
|
||||
this.waitForNotesSavedIID_ = shim.setInterval(() => {
|
||||
if (!this.props.hasNotesBeingSaved) {
|
||||
logger.info('[appClose] Notes saved, now sending canClose: true');
|
||||
shim.clearInterval(this.waitForNotesSavedIID_);
|
||||
this.waitForNotesSavedIID_ = null;
|
||||
void sendCanClose(true);
|
||||
|
||||
@@ -200,11 +200,6 @@ function menuItemSetEnabled(id: string, enabled: boolean) {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
const menuItem = menu.getMenuItemById(id);
|
||||
if (!menuItem) return;
|
||||
// Don't disable menu items that have a role (e.g. copy, paste, cut,
|
||||
// selectAll). Since Electron 40, disabling a role-based menu item also
|
||||
// prevents the native role behaviour, which breaks clipboard operations
|
||||
// in non-editor input fields such as the Settings screen.
|
||||
if (!enabled && menuItem.role) return;
|
||||
menuItem.enabled = enabled;
|
||||
}
|
||||
|
||||
@@ -828,12 +823,6 @@ function useMenu(props: Props) {
|
||||
Setting.incValue('windowContentZoomFactor', -10);
|
||||
},
|
||||
accelerator: 'CommandOrControl+-',
|
||||
}, {
|
||||
type: 'separator',
|
||||
visible: shim.isMac(),
|
||||
}, {
|
||||
role: 'togglefullscreen',
|
||||
visible: shim.isMac(),
|
||||
}],
|
||||
},
|
||||
go: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AppState, AppStateRoute } from '../app.reducer';
|
||||
import bridge from '../services/bridge';
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { WindowIdContext } from './NewWindowOrIFrame';
|
||||
import useCtrlWheelZoom from './hooks/useCtrlWheelZoom';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of code from before rule was applied
|
||||
type ScreenProps = any;
|
||||
@@ -98,6 +99,7 @@ const NavigatorComponent: React.FC<Props> = props => {
|
||||
|
||||
useWindowTitleManager(screenInfo);
|
||||
useWindowRefocusManager(route);
|
||||
useCtrlWheelZoom();
|
||||
const size = useContainerSize(container);
|
||||
|
||||
if (!route) throw new Error('Route must not be null');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, createContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
@@ -39,7 +40,7 @@ const useDocument = (
|
||||
|
||||
useEffect(() => {
|
||||
let openedWindow: Window|null = null;
|
||||
let unmounted = false;
|
||||
const unmounted = false;
|
||||
if (iframeElement) {
|
||||
setDoc(iframeElement?.contentWindow?.document);
|
||||
} else if (mode === WindowMode.NewWindow) {
|
||||
@@ -51,16 +52,11 @@ const useDocument = (
|
||||
void (async () => {
|
||||
while (!unmounted) {
|
||||
await new Promise<void>(resolve => {
|
||||
setTimeout(() => resolve(), 2000);
|
||||
shim.setTimeout(() => resolve(), 2000);
|
||||
});
|
||||
|
||||
// Re-check after sleep to avoid duplicate WINDOW_CLOSE if IPC already fired.
|
||||
if (unmounted) break;
|
||||
|
||||
if (openedWindow?.closed) {
|
||||
// Null out doc first so React stops rendering into the destroyed window
|
||||
// before WINDOW_CLOSE triggers unmounting (prevents renderer crash on Windows).
|
||||
setDoc(null);
|
||||
onCloseRef.current?.();
|
||||
openedWindow = null;
|
||||
break;
|
||||
}
|
||||
@@ -69,8 +65,6 @@ const useDocument = (
|
||||
}
|
||||
|
||||
return () => {
|
||||
unmounted = true;
|
||||
|
||||
// Delay: Closing immediately causes Electron to crash
|
||||
setTimeout(() => {
|
||||
if (!openedWindow?.closed) {
|
||||
@@ -94,6 +88,10 @@ const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded) => {
|
||||
useEffect(() => {
|
||||
if (!doc) return;
|
||||
|
||||
doc.open();
|
||||
doc.write('<!DOCTYPE html><html><head></head><body></body></html>');
|
||||
doc.close();
|
||||
|
||||
const cssUrls = [
|
||||
'style.min.css',
|
||||
];
|
||||
|
||||
@@ -22,21 +22,18 @@ interface KeyToLabelMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
let markupToHtml_: ReturnType<typeof markupLanguageUtils.newMarkupToHtml> = null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
let markupToHtml_: any = null;
|
||||
function markupToHtml() {
|
||||
if (markupToHtml_) return markupToHtml_;
|
||||
markupToHtml_ = markupLanguageUtils.newMarkupToHtml();
|
||||
return markupToHtml_;
|
||||
}
|
||||
|
||||
interface CounterResult {
|
||||
words: number;
|
||||
all: number;
|
||||
characters: number;
|
||||
}
|
||||
|
||||
function countElements(text: string, wordSetter: React.Dispatch<React.SetStateAction<number>>, characterSetter: React.Dispatch<React.SetStateAction<number>>, characterNoSpaceSetter: React.Dispatch<React.SetStateAction<number>>, cjkCharacterSetter: React.Dispatch<React.SetStateAction<number>>, lineSetter: React.Dispatch<React.SetStateAction<number>>) {
|
||||
Countable.count(text, (counter: CounterResult) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
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);
|
||||
@@ -56,7 +53,8 @@ function formatReadTime(readTimeMinutes: number) {
|
||||
|
||||
export default function NoteContentPropertiesDialog(props: NoteContentPropertiesDialogProps) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const tableBodyComps: React.JSX.Element[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const tableBodyComps: any[] = [];
|
||||
// For the source Markdown
|
||||
const [lines, setLines] = useState<number>(0);
|
||||
const [words, setWords] = useState<number>(0);
|
||||
|
||||
@@ -45,8 +45,8 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const pasteEventHandler = (_editor: unknown, ...args: unknown[]) => {
|
||||
const event = args[0] as Event;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const pasteEventHandler = (_editor: any, event: Event) => {
|
||||
props.onEditorPaste(event);
|
||||
};
|
||||
|
||||
|
||||
@@ -1493,23 +1493,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearInheritedCheckedStateOnChecklistEnter = () => {
|
||||
const currentNode = editor.selection.getStart();
|
||||
const currentListItem = editor.dom.getParent(currentNode, 'li') as HTMLLIElement;
|
||||
if (!currentListItem) return;
|
||||
|
||||
const parentChecklist = editor.dom.getParent(currentListItem, 'ul.joplin-checklist');
|
||||
if (!parentChecklist) return;
|
||||
|
||||
if (!currentListItem.classList.contains('checked')) return;
|
||||
|
||||
const textContent = (currentListItem.textContent ?? '').replace(/\u200B/g, '').trim();
|
||||
if (textContent !== '') return;
|
||||
|
||||
currentListItem.classList.remove('checked');
|
||||
onChangeHandler();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
async function onKeyDown(event: any) {
|
||||
// It seems "paste as text" is handled automatically on Windows and Linux,
|
||||
@@ -1525,13 +1508,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
event.preventDefault();
|
||||
pasteAsPlainText(null);
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.isComposing) {
|
||||
shim.setTimeout(() => {
|
||||
if (!editor || !editor.getDoc()) return;
|
||||
clearInheritedCheckedStateOnChecklistEnter();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function onPasteAsText() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import { RefObject } from 'react';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'focusElementNoteTitle',
|
||||
@@ -9,7 +8,8 @@ export const declaration: CommandDeclaration = {
|
||||
parentLabel: () => _('Focus'),
|
||||
};
|
||||
|
||||
export const runtime = (comp: { titleInputRef: RefObject<HTMLInputElement> }): CommandRuntime => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
return {
|
||||
execute: async () => {
|
||||
if (!comp.titleInputRef.current) return;
|
||||
|
||||
@@ -6,12 +6,10 @@ const baseContext: Record<string, any> = {
|
||||
modalDialogVisible: false,
|
||||
gotoAnythingVisible: false,
|
||||
markdownEditorPaneVisible: true,
|
||||
markdownViewerPaneVisible: false,
|
||||
oneNoteSelected: true,
|
||||
noteIsMarkdown: true,
|
||||
noteIsReadOnly: false,
|
||||
richTextEditorVisible: false,
|
||||
hasActivePluginEditor: false,
|
||||
};
|
||||
|
||||
describe('editorCommandDeclarations', () => {
|
||||
@@ -100,38 +98,9 @@ describe('editorCommandDeclarations', () => {
|
||||
{
|
||||
textBold: false,
|
||||
textPaste: false,
|
||||
textCopy: true,
|
||||
textSelectAll: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
// Viewer-only mode (no editor pane visible, only the rendered viewer)
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: false,
|
||||
markdownViewerPaneVisible: true,
|
||||
},
|
||||
{
|
||||
textCopy: true,
|
||||
textSelectAll: true,
|
||||
textCut: false,
|
||||
textPaste: false,
|
||||
textBold: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
// Viewer-only mode with a read-only note
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: false,
|
||||
markdownViewerPaneVisible: true,
|
||||
noteIsReadOnly: true,
|
||||
},
|
||||
{
|
||||
textCopy: true,
|
||||
textSelectAll: true,
|
||||
textCut: false,
|
||||
textPaste: false,
|
||||
|
||||
// TODO: textCopy should be enabled in read-only notes:
|
||||
// textCopy: false,
|
||||
},
|
||||
],
|
||||
])('should correctly determine whether command is enabled (case %#)', (context, expectedStates) => {
|
||||
|
||||
@@ -10,31 +10,18 @@ const workWithHtmlNotes = [
|
||||
'textSelectAll',
|
||||
];
|
||||
|
||||
// Commands that should remain enabled in viewer mode and when the note is read-only.
|
||||
const worksInViewerAndReadOnlyMode = [
|
||||
'textCopy',
|
||||
'textSelectAll',
|
||||
];
|
||||
|
||||
export const enabledCondition = (commandName: string) => {
|
||||
const markdownEditorOnly = !Object.keys(joplinCommandToTinyMceCommands).includes(commandName);
|
||||
const noteMustBeMarkdown = !workWithHtmlNotes.includes(commandName);
|
||||
const allowInViewerAndReadOnlyMode = worksInViewerAndReadOnlyMode.includes(commandName);
|
||||
|
||||
const editorPaneCondition = markdownEditorOnly
|
||||
? '(markdownEditorPaneVisible || hasActivePluginEditor)'
|
||||
: allowInViewerAndReadOnlyMode
|
||||
? '(markdownEditorPaneVisible || richTextEditorVisible || markdownViewerPaneVisible || hasActivePluginEditor)'
|
||||
: '(markdownEditorPaneVisible || richTextEditorVisible || hasActivePluginEditor)';
|
||||
|
||||
const output = [
|
||||
// gotoAnythingVisible: Enable if the command palette (which is a modal dialog) is visible
|
||||
'(!modalDialogVisible || gotoAnythingVisible)',
|
||||
|
||||
editorPaneCondition,
|
||||
markdownEditorOnly ? 'markdownEditorPaneVisible' : '(markdownEditorPaneVisible || richTextEditorVisible)',
|
||||
'oneNoteSelected',
|
||||
noteMustBeMarkdown ? 'noteIsMarkdown' : '',
|
||||
allowInViewerAndReadOnlyMode ? '' : '!noteIsReadOnly',
|
||||
'!noteIsReadOnly',
|
||||
];
|
||||
|
||||
return output.filter(c => !!c).join(' && ');
|
||||
|
||||
@@ -90,19 +90,6 @@ export function resourcesStatus(resourceInfos: any) {
|
||||
return joplinRendererUtils.resourceStatusName(lowestIndex);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const clipboardImageToResource = async (image: any, mime: string) => {
|
||||
const fileExt = mimeUtils.toFileExtension(mime);
|
||||
const filePath = `${Setting.value('tempDir')}/${md5(Date.now())}.${fileExt}`;
|
||||
await shim.writeImageToFile(image, mime, filePath);
|
||||
try {
|
||||
const md = await commandAttachFileToBody('', [filePath]);
|
||||
return md;
|
||||
} finally {
|
||||
await shim.fsDriver().remove(filePath);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export async function getResourcesFromPasteEvent(event: any) {
|
||||
const output = [];
|
||||
@@ -117,22 +104,19 @@ export async function getResourcesFromPasteEvent(event: any) {
|
||||
continue;
|
||||
}
|
||||
if (event) event.preventDefault();
|
||||
const md = await clipboardImageToResource(clipboard.readImage(), format);
|
||||
|
||||
const image = clipboard.readImage();
|
||||
|
||||
const fileExt = mimeUtils.toFileExtension(format);
|
||||
const filePath = `${Setting.value('tempDir')}/${md5(Date.now())}.${fileExt}`;
|
||||
|
||||
await shim.writeImageToFile(image, format, filePath);
|
||||
const md = await commandAttachFileToBody('', [filePath]);
|
||||
await shim.fsDriver().remove(filePath);
|
||||
|
||||
if (md) output.push(md);
|
||||
}
|
||||
}
|
||||
|
||||
// Some applications (e.g. macshot) copy images to the clipboard without
|
||||
// an image/* format, but clipboard.readImage() can still read them.
|
||||
if (!output.length) {
|
||||
const image = clipboard.readImage();
|
||||
if (!image.isEmpty()) {
|
||||
if (event) event.preventDefault();
|
||||
const md = await clipboardImageToResource(image, 'image/png');
|
||||
if (md) output.push(md);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import useOnNoteDoubleClick from './utils/useOnNoteDoubleClick';
|
||||
import useAutoScroll from './utils/useAutoScroll';
|
||||
import useRefocusOnDeletion from './utils/useRefocusOnDeletion';
|
||||
|
||||
const commands = {
|
||||
focusElementNoteList,
|
||||
@@ -74,6 +75,7 @@ const NoteList = (props: Props) => {
|
||||
|
||||
const { activeNoteId, setActiveNoteId } = useActiveDescendantId(props.selectedFolderId, props.selectedNoteIds);
|
||||
const focusNote = useFocusNote(listRef, props.notes, makeItemIndexVisible, setActiveNoteId);
|
||||
useRefocusOnDeletion(props.notes.length, props.selectedNoteIds, props.focusedField, props.selectedFolderId, focusNote);
|
||||
|
||||
const moveNote = useMoveNote(
|
||||
props.notesParentType,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import useRefocusOnDeletion from './useRefocusOnDeletion';
|
||||
|
||||
describe('useRefocusOnDeletion', () => {
|
||||
it('should refocus when a note is deleted in the same folder', () => {
|
||||
const focusNote = jest.fn();
|
||||
const { rerender } = renderHook(
|
||||
({ noteCount }: { noteCount: number }) =>
|
||||
useRefocusOnDeletion(noteCount, ['note-1'], '', 'folder-1', focusNote),
|
||||
{ initialProps: { noteCount: 3 } },
|
||||
);
|
||||
rerender({ noteCount: 2 });
|
||||
expect(focusNote).toHaveBeenCalledWith('note-1');
|
||||
});
|
||||
|
||||
test.each([
|
||||
['note count increases', 2, 3, '', ['note-1']],
|
||||
['another field has focus', 3, 2, 'editor', ['note-1']],
|
||||
['multiple notes are selected', 3, 2, '', ['note-1', 'note-2']],
|
||||
])('should not refocus when %s', (_label, initialCount, newCount, focusedField, noteIds) => {
|
||||
const focusNote = jest.fn();
|
||||
const { rerender } = renderHook(
|
||||
({ noteCount }: { noteCount: number }) =>
|
||||
useRefocusOnDeletion(noteCount, noteIds, focusedField, 'folder-1', focusNote),
|
||||
{ initialProps: { noteCount: initialCount } },
|
||||
);
|
||||
rerender({ noteCount: newCount });
|
||||
expect(focusNote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not refocus when switching to a folder with fewer notes', () => {
|
||||
const focusNote = jest.fn();
|
||||
const { rerender } = renderHook(
|
||||
({ noteCount, folderId }: { noteCount: number; folderId: string }) =>
|
||||
useRefocusOnDeletion(noteCount, ['note-1'], '', folderId, focusNote),
|
||||
{ initialProps: { noteCount: 3, folderId: 'folder-1' } },
|
||||
);
|
||||
rerender({ noteCount: 2, folderId: 'folder-2' });
|
||||
expect(focusNote).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useEffect } from 'react';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
const useRefocusOnDeletion = (
|
||||
noteCount: number,
|
||||
selectedNoteIds: string[],
|
||||
focusedField: string,
|
||||
selectedFolderId: string,
|
||||
focusNote: (noteId: string)=> void,
|
||||
) => {
|
||||
const previousNoteCount = usePrevious(noteCount, 0);
|
||||
const previousFolderId = usePrevious(selectedFolderId, '');
|
||||
useEffect(() => {
|
||||
const noteWasRemoved = noteCount < previousNoteCount;
|
||||
const folderDidNotChange = selectedFolderId === previousFolderId;
|
||||
if (noteWasRemoved && folderDidNotChange && selectedNoteIds.length === 1 && !focusedField) {
|
||||
focusNote(selectedNoteIds[0]);
|
||||
}
|
||||
}, [noteCount, previousNoteCount, selectedNoteIds, focusedField, selectedFolderId, previousFolderId, focusNote]);
|
||||
};
|
||||
export default useRefocusOnDeletion;
|
||||
@@ -54,16 +54,7 @@ export default (props: Props) => {
|
||||
classes.push(props.isReverse ? 'fa-chevron-down' : 'fa-chevron-up');
|
||||
chevron = <i className={classes.join(' ')}></i>;
|
||||
}
|
||||
const title = getColumnTitle(column.name);
|
||||
let titleElement: React.ReactNode = title;
|
||||
|
||||
if (column.name === 'note.checkboxes') {
|
||||
titleElement = <i className="fas fa-adjust" aria-label={title} title={title}></i>;
|
||||
} else if (column.name === 'note.is_todo') {
|
||||
titleElement = <i className="fas fa-check" aria-label={title} title={title}></i>;
|
||||
}
|
||||
|
||||
return <span className="titlewrapper">{titleElement}{chevron}</span>;
|
||||
return <span className="titlewrapper">{getColumnTitle(column.name, true)}{chevron}</span>;
|
||||
};
|
||||
|
||||
const renderResizer = () => {
|
||||
@@ -86,7 +77,6 @@ export default (props: Props) => {
|
||||
draggable={true}
|
||||
className={classes.join(' ')}
|
||||
style={style}
|
||||
title={getColumnTitle(column.name)}
|
||||
onClick={onClick}
|
||||
onDragStart={props.onDragStart}
|
||||
onDragOver={props.onDragOver}
|
||||
|
||||
@@ -16,6 +16,14 @@ const titles: Record<ColumnName, ()=> string> = {
|
||||
'note.user_updated_time': () => _('Updated'),
|
||||
};
|
||||
|
||||
export default (name: ColumnName) => {
|
||||
return titles[name]();
|
||||
const titlesForHeader: Partial<Record<ColumnName, ()=> string>> = {
|
||||
'note.checkboxes': () => '◐',
|
||||
'note.is_todo': () => '✓',
|
||||
};
|
||||
|
||||
export default (name: ColumnName, forHeader = false) => {
|
||||
let fn: ()=> string = null;
|
||||
if (forHeader) fn = titlesForHeader[name];
|
||||
if (!fn) fn = titles[name];
|
||||
return fn ? fn() : name;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ interface RenderedNote {
|
||||
html: string;
|
||||
}
|
||||
|
||||
const hashContent = (content: unknown) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const hashContent = (content: any) => {
|
||||
return createHash('sha1').update(JSON.stringify(content)).digest('hex');
|
||||
};
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
const styles = this.styles(this.props.themeId);
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const labelText = this.formatLabel(key);
|
||||
const labelComp = <label htmlFor={uniqueId(key)} style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{labelText}</label>;
|
||||
const labelComp = <label htmlFor={uniqueId(key)} role='rowheader' style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{labelText}</label>;
|
||||
let controlComp = null;
|
||||
let editComp = null;
|
||||
let editCompHandler = null;
|
||||
@@ -422,11 +422,11 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'inline-block',
|
||||
};
|
||||
controlComp = displayedValue ? (
|
||||
controlComp = (
|
||||
<a href="#" onClick={() => bridge().openExternal(url)} style={urlStyle}>
|
||||
{displayedValue}
|
||||
</a>
|
||||
) : null;
|
||||
);
|
||||
} else if (key === 'revisionsLink') {
|
||||
controlComp = (
|
||||
<a href="#" onClick={this.revisionsLink_click} style={theme.urlStyle}>
|
||||
@@ -468,10 +468,10 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={key} style={theme.controlBox} className="note-property-box">
|
||||
<th>{labelComp}</th>
|
||||
<td>{controlComp} {editComp}</td>
|
||||
</tr>
|
||||
<div role='row' key={key} style={theme.controlBox} className="note-property-box">
|
||||
{labelComp}
|
||||
<span role='cell'>{controlComp} {editComp}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -497,12 +497,10 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<Dialog onCancel={this.props.onClose}>
|
||||
<h1 style={theme.dialogTitle} id='note-properties-dialog-title'>{_('Note properties')}</h1>
|
||||
<table aria-labelledby='note-properties-dialog-title'>
|
||||
<tbody>
|
||||
{noteComps}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={theme.dialogTitle} id='note-properties-dialog-title'>{_('Note properties')}</div>
|
||||
<div role='table' aria-labelledby='note-properties-dialog-title'>
|
||||
{noteComps}
|
||||
</div>
|
||||
<DialogButtonRow
|
||||
themeId={this.props.themeId}
|
||||
okButtonShow={!this.isReadOnly()}
|
||||
|
||||
@@ -5,22 +5,28 @@ import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
onNext: ()=> void;
|
||||
onPrevious: ()=> void;
|
||||
onClose: ()=> void;
|
||||
onChange: (query: string)=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onNext: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onPrevious: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClose: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onChange: Function;
|
||||
query: string;
|
||||
searching: boolean;
|
||||
resultCount: number;
|
||||
selectedIndex: number;
|
||||
visiblePanes: string[];
|
||||
editorType: string;
|
||||
style: React.CSSProperties;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
}
|
||||
|
||||
class NoteSearchBar extends React.Component<Props> {
|
||||
|
||||
private backgroundColor: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private backgroundColor: any;
|
||||
private searchInputRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
public constructor(props: Props) {
|
||||
@@ -50,7 +56,8 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
return style;
|
||||
}
|
||||
|
||||
public buttonIconComponent(iconName: string, clickHandler: ()=> void, isEnabled: boolean) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public buttonIconComponent(iconName: string, clickHandler: any, isEnabled: boolean) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const searchButton = {
|
||||
@@ -78,12 +85,14 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
private searchInput_change(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private searchInput_change(event: any) {
|
||||
const query = event.currentTarget.value;
|
||||
this.triggerOnChange(query);
|
||||
}
|
||||
|
||||
private searchInput_keyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private searchInput_keyDown(event: any) {
|
||||
if (event.keyCode === 13) {
|
||||
// ENTER
|
||||
event.preventDefault();
|
||||
@@ -105,7 +114,7 @@ class NoteSearchBar extends React.Component<Props> {
|
||||
if (event.keyCode === 70) {
|
||||
// F key
|
||||
if (event.ctrlKey) {
|
||||
event.currentTarget.select();
|
||||
event.target.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ interface Props {
|
||||
onDomReady: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onIpcMessage: Function;
|
||||
viewerStyle: React.CSSProperties;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
viewerStyle: any;
|
||||
contentMaxWidth?: number;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import { AppState } from '../../app.reducer';
|
||||
|
||||
interface NoteToolbarProps {
|
||||
themeId: number;
|
||||
style: React.CSSProperties;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
toolbarButtonInfos: ToolbarItem[];
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const { themeStyle } = require('@joplin/lib/theme');
|
||||
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
themeId: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -24,8 +24,10 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const log = (s: string) => {
|
||||
this.setState((state: { authLog: { key: string; text: string }[] }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const log = (s: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
this.setState((state: any) => {
|
||||
const authLog = state.authLog.slice();
|
||||
authLog.push({ key: (Date.now() + Math.random()).toString(), text: s });
|
||||
return { authLog: authLog };
|
||||
@@ -36,7 +38,8 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
|
||||
const syncTarget = reg.syncTarget(syncTargetId);
|
||||
const oneDriveApiUtils = new OneDriveApiNodeUtils(syncTarget.api());
|
||||
const auth = await oneDriveApiUtils.oauthDance({
|
||||
log: (s: string) => log(s),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
log: (s: any) => log(s),
|
||||
});
|
||||
|
||||
Setting.setValue(`sync.${syncTargetId}.auth`, auth ? JSON.stringify(auth) : null);
|
||||
@@ -82,7 +85,8 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: { settings: { theme: number } }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const mapStateToProps = (state: any) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
|
||||
@@ -277,52 +277,8 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
style={styles.dateTimeInput}
|
||||
/>;
|
||||
} else if (this.props.inputType === 'tags') {
|
||||
const uniqueAutocomplete = [];
|
||||
const seenLabels = new Set();
|
||||
const autocompleteOptions = this.props.autocomplete || [];
|
||||
for (const option of autocompleteOptions) {
|
||||
const key = (option.label || '').trim().normalize('NFC').toLowerCase();
|
||||
if (!seenLabels.has(key)) {
|
||||
uniqueAutocomplete.push(option);
|
||||
seenLabels.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
inputComp = <CreatableSelect
|
||||
className="tag-selector"
|
||||
onMenuOpen={this.select_menuOpen}
|
||||
onMenuClose={this.select_menuClose}
|
||||
styles={styles.select}
|
||||
theme={styles.selectTheme}
|
||||
ref={this.answerInput_}
|
||||
value={this.state.answer}
|
||||
placeholder=""
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
components={makeAnimated() as any}
|
||||
isMulti={true}
|
||||
isClearable={false}
|
||||
backspaceRemovesValue={true}
|
||||
options={uniqueAutocomplete}
|
||||
onChange={onSelectChange}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onKeyDown={(event: any) => onKeyDown(event)}
|
||||
filterOption={(option, rawInput) => {
|
||||
const input = (rawInput || '').trim().normalize('NFC').toLowerCase();
|
||||
const label = (option.label || '').trim().normalize('NFC').toLowerCase();
|
||||
return label.includes(input);
|
||||
}}
|
||||
isValidNewOption={(inputValue, _selectValue, selectOptions) => {
|
||||
const input = (inputValue || '').trim().normalize('NFC').toLowerCase();
|
||||
if (!input) return false;
|
||||
|
||||
// If it matches an existing option (case-insensitive + normalized), it's not a valid "new" option
|
||||
const exists = selectOptions.some(option => {
|
||||
return (option.label || '').trim().normalize('NFC').toLowerCase() === input;
|
||||
});
|
||||
return !exists;
|
||||
}}
|
||||
/>;
|
||||
inputComp = <CreatableSelect className="tag-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} value={this.state.answer} placeholder="" components={makeAnimated() as any} isMulti={true} isClearable={false} backspaceRemovesValue={true} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
} else if (this.props.inputType === 'dropdown') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
inputComp = <Select className="item-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} components={makeAnimated() as any} value={this.props.answer} defaultValue={this.props.defaultValue} isClearable={false} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
||||
import areAllFoldersCollapsed from '@joplin/lib/models/utils/areAllFoldersCollapsed';
|
||||
import getCanBeCollapsedFolderIds from '@joplin/lib/models/utils/getCanBeCollapsedFolderIds';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
@@ -49,11 +48,6 @@ const FolderAndTagList: React.FC<Props> = props => {
|
||||
return areAllFoldersCollapsed(props.folders, props.collapsedFolderIds);
|
||||
}, [props.collapsedFolderIds, props.folders]);
|
||||
|
||||
const hasSubFolders = useMemo(() => {
|
||||
return getCanBeCollapsedFolderIds(props.folders).length > 0;
|
||||
}, [props.folders]);
|
||||
|
||||
|
||||
const listContainerRef = useRef<HTMLDivElement|null>(null);
|
||||
const onRenderItem = useOnRenderItem({
|
||||
...props,
|
||||
@@ -82,7 +76,7 @@ const FolderAndTagList: React.FC<Props> = props => {
|
||||
const listHeight = useElementHeight(itemListContainer);
|
||||
const listStyle = useMemo(() => ({ height: listHeight }), [listHeight]);
|
||||
|
||||
const onRenderContentWrapper = useOnRenderListWrapper({ allFoldersCollapsed, selectedIndex, onKeyDown: onKeyEventHandler, hasSubFolders });
|
||||
const onRenderContentWrapper = useOnRenderListWrapper({ allFoldersCollapsed, selectedIndex, onKeyDown: onKeyEventHandler });
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton, StyledRoot } from './styles';
|
||||
import { ButtonLevel } from '../Button/Button';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Synchronizer from '@joplin/lib/Synchronizer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { StateDecryptionWorker, StateResourceFetcher } from '@joplin/lib/reducer';
|
||||
@@ -11,6 +11,8 @@ import { connect } from 'react-redux';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Dispatch } from 'redux';
|
||||
import FolderAndTagList from './FolderAndTagList';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import time from '@joplin/lib/time';
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -21,26 +23,18 @@ interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
syncReport: any;
|
||||
syncStarted: boolean;
|
||||
syncPending: boolean;
|
||||
syncReportIsVisible: boolean;
|
||||
syncReportLogExpanded: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The generated report does not currently have a type
|
||||
const syncCompletedWithoutError = (syncReport: any) => {
|
||||
return syncReport.completedTime && (!syncReport.errors || !syncReport.errors.length);
|
||||
};
|
||||
|
||||
const SidebarComponent = (props: Props) => {
|
||||
const renderSynchronizeButton = (type: string) => {
|
||||
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
|
||||
const nothingToSync = type === 'sync' && !props.syncPending && syncCompletedWithoutError(props.syncReport);
|
||||
const iconName = nothingToSync ? 'fas fa-check' : 'icon-sync';
|
||||
|
||||
return (
|
||||
<StyledSynchronizeButton
|
||||
level={ButtonLevel.SidebarSecondary}
|
||||
className={`sidebar-sync-button ${type === 'sync' ? '' : '-syncing'} ${nothingToSync ? '-synced' : ''}`}
|
||||
iconName={iconName}
|
||||
className={`sidebar-sync-button ${type === 'sync' ? '' : '-syncing'}`}
|
||||
iconName="icon-sync"
|
||||
key="sync_button"
|
||||
title={label}
|
||||
onClick={() => {
|
||||
@@ -62,46 +56,55 @@ const SidebarComponent = (props: Props) => {
|
||||
resourceFetcherText = _('Fetching resources: %d/%d', props.resourceFetcher.fetchingCount, props.resourceFetcher.toFetchCount);
|
||||
}
|
||||
|
||||
const syncReportExpanded = props.syncReportLogExpanded;
|
||||
|
||||
const toggleSyncReport = useCallback(() => {
|
||||
Setting.setValue('syncReportLogExpanded', !syncReportExpanded);
|
||||
}, [syncReportExpanded]);
|
||||
|
||||
const lines = Synchronizer.reportToLines(props.syncReport);
|
||||
if (resourceFetcherText) lines.push(resourceFetcherText);
|
||||
if (decryptionReportText) lines.push(decryptionReportText);
|
||||
const syncReportText = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
syncReportText.push(
|
||||
<StyledSyncReportText key={i}>
|
||||
{lines[i]}
|
||||
</StyledSyncReportText>,
|
||||
);
|
||||
}
|
||||
|
||||
const completedTime = props.syncReport && props.syncReport.completedTime
|
||||
? time.formatMsToLocal(props.syncReport.completedTime)
|
||||
: null;
|
||||
|
||||
const syncButton = renderSynchronizeButton(props.syncStarted ? 'cancel' : 'sync');
|
||||
|
||||
const hasSyncReport = syncReportText.length > 0;
|
||||
// Show toggle when there are log lines or a completed timestamp
|
||||
const hasContent = lines.length > 0 || completedTime;
|
||||
|
||||
const syncReportComp = !hasSyncReport || !props.syncReportIsVisible ? null : (
|
||||
<StyledSyncReport key="sync_report" id="sync-report">
|
||||
{syncReportText}
|
||||
</StyledSyncReport>
|
||||
);
|
||||
|
||||
const syncReportToggle = (
|
||||
// Toggle to show/hide sync log output
|
||||
const toggleButton = hasContent ? (
|
||||
<button
|
||||
className="sync-report-toggle"
|
||||
style={{ color: theme.color2 }}
|
||||
onClick={() => Setting.toggle('syncReportIsVisible')}
|
||||
aria-label={_('Sync report')}
|
||||
aria-expanded={props.syncReportIsVisible}
|
||||
aria-controls="sync-report"
|
||||
className="sidebar-sync-toggle"
|
||||
onClick={toggleSyncReport}
|
||||
aria-expanded={syncReportExpanded}
|
||||
aria-label={syncReportExpanded ? _('Hide sync log') : _('Show sync log')}
|
||||
title={syncReportExpanded ? _('Hide sync log') : _('Show sync log')}
|
||||
>
|
||||
<i className={`fas fa-chevron-${props.syncReportIsVisible ? 'down' : 'up'}`}/>
|
||||
<i className={`fas fa-caret-${syncReportExpanded ? 'down' : 'up'}`} />
|
||||
{!syncReportExpanded && completedTime ? <span className="timestamp">{_('Last sync: %s', completedTime)}</span> : ''}
|
||||
</button>
|
||||
);
|
||||
) : null;
|
||||
|
||||
// Sync log output, only visible when expanded
|
||||
const syncReportComp = (syncReportExpanded && lines.length > 0) ? (
|
||||
<StyledSyncReport key="sync_report">
|
||||
{lines.map((line, i) => (
|
||||
<StyledSyncReportText key={i}>
|
||||
{line}
|
||||
</StyledSyncReportText>
|
||||
))}
|
||||
</StyledSyncReport>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<StyledRoot className='sidebar _scrollbar2' role='navigation' aria-label={_('Sidebar')}>
|
||||
<div style={{ flex: 1 }}><FolderAndTagList/></div>
|
||||
<div style={{ flex: 1 }}><FolderAndTagList /></div>
|
||||
<div style={{ flex: 0, padding: theme.mainPadding }}>
|
||||
{syncReportToggle}
|
||||
{toggleButton}
|
||||
{syncReportComp}
|
||||
{syncButton}
|
||||
</div>
|
||||
@@ -113,7 +116,6 @@ const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
searches: state.searches,
|
||||
syncStarted: state.syncStarted,
|
||||
syncPending: state.syncPending,
|
||||
syncReport: state.syncReport,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
selectedSmartFilterId: state.selectedSmartFilterId,
|
||||
@@ -122,7 +124,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
collapsedFolderIds: state.collapsedFolderIds,
|
||||
decryptionWorker: state.decryptionWorker,
|
||||
resourceFetcher: state.resourceFetcher,
|
||||
syncReportIsVisible: state.settings.syncReportIsVisible,
|
||||
syncReportLogExpanded: state.settings.syncReportLogExpanded,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ interface Props {
|
||||
selectedIndex: number;
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
allFoldersCollapsed: boolean;
|
||||
hasSubFolders: boolean;
|
||||
}
|
||||
|
||||
const onAddFolderButtonClick = () => {
|
||||
@@ -20,7 +19,6 @@ const onToggleAllFolders = (allFoldersCollapsed: boolean) => {
|
||||
|
||||
interface CollapseExpandAllButtonProps {
|
||||
allFoldersCollapsed: boolean;
|
||||
hasSubFolders: boolean;
|
||||
}
|
||||
|
||||
const CollapseExpandAllButton = (props: CollapseExpandAllButtonProps) => {
|
||||
@@ -29,12 +27,7 @@ const CollapseExpandAllButton = (props: CollapseExpandAllButtonProps) => {
|
||||
const icon = props.allFoldersCollapsed ? 'far fa-caret-square-right' : 'far fa-caret-square-down';
|
||||
const label = props.allFoldersCollapsed ? _('Expand all notebooks') : _('Collapse all notebooks');
|
||||
|
||||
return <button
|
||||
onClick={() => onToggleAllFolders(props.allFoldersCollapsed)}
|
||||
className={`sidebar-header-button -collapseall ${props.hasSubFolders ? '' : '-disabled'}`}
|
||||
title={label}
|
||||
disabled={!props.hasSubFolders}
|
||||
>
|
||||
return <button onClick={() => onToggleAllFolders(props.allFoldersCollapsed)} className='sidebar-header-button -collapseall' title={label}>
|
||||
<i
|
||||
aria-label={label}
|
||||
role='img'
|
||||
@@ -62,7 +55,7 @@ const useOnRenderListWrapper = (props: Props) => {
|
||||
const listHasValidSelection = props.selectedIndex >= 0;
|
||||
const allowContainerFocus = !listHasValidSelection;
|
||||
return <>
|
||||
<CollapseExpandAllButton allFoldersCollapsed={props.allFoldersCollapsed} hasSubFolders={props.hasSubFolders}/>
|
||||
<CollapseExpandAllButton allFoldersCollapsed={props.allFoldersCollapsed}/>
|
||||
<NewFolderButton/>
|
||||
<div
|
||||
role='tree'
|
||||
@@ -73,7 +66,7 @@ const useOnRenderListWrapper = (props: Props) => {
|
||||
{...listItems}
|
||||
</div>
|
||||
</>;
|
||||
}, [props.selectedIndex, props.onKeyDown, props.allFoldersCollapsed, props.hasSubFolders]);
|
||||
}, [props.selectedIndex, props.onKeyDown, props.allFoldersCollapsed]);
|
||||
};
|
||||
|
||||
export default useOnRenderListWrapper;
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
@use 'styles/sidebar-header-container.scss';
|
||||
@use 'styles/sidebar-spacer-item.scss';
|
||||
@use 'styles/sidebar-header-button.scss';
|
||||
@use 'styles/sidebar-sync-button.scss';
|
||||
@use 'styles/sidebar-sync-button.scss';
|
||||
@use 'styles/sidebar-sync-toggle.scss';
|
||||
@@ -13,12 +13,12 @@
|
||||
font-size: var(--joplin-toolbar-icon-size);
|
||||
color: var(--joplin-color2);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
&:hover {
|
||||
color: var(--joplin-color-hover2);
|
||||
background: none;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
&:active {
|
||||
color: var(--joplin-color-active2);
|
||||
background: none;
|
||||
}
|
||||
@@ -26,8 +26,4 @@
|
||||
&.-collapseall {
|
||||
right: 25px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,25 +5,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes icon-fade-in-a {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes icon-fade-in-b {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.sidebar-sync-button > .icon {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px !important;
|
||||
height: 16px;
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
|
||||
.sidebar-sync-button {
|
||||
&.-syncing > .icon {
|
||||
animation: icon-infinite-rotation 1s linear infinite;
|
||||
@@ -32,29 +13,4 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.-syncing).-synced > .icon {
|
||||
animation: icon-fade-in-a 300ms ease-in-out;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
&:not(.-syncing):not(.-synced) > .icon {
|
||||
animation: icon-fade-in-b 300ms ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.sync-report-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.sidebar-sync-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--joplin-color2);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
width: 100%;
|
||||
font-size: calc(var(--joplin-font-size) * 1.6);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
>.timestamp {
|
||||
font-size: 0.6em;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,12 @@ import { getCollator, getCollatorLocale } from '@joplin/lib/models/utils/getColl
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
|
||||
interface TagData {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
style: React.CSSProperties;
|
||||
items: TagData[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
style: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
items: any[];
|
||||
}
|
||||
|
||||
function TagList(props: Props) {
|
||||
@@ -37,7 +34,8 @@ function TagList(props: Props) {
|
||||
const tags = useMemo(() => {
|
||||
const output = props.items.slice();
|
||||
const collator = getCollator(collatorLocale);
|
||||
output.sort((a: TagData, b: TagData) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
output.sort((a: any, b: any) => {
|
||||
return collator.compare(a.title, b.title);
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ class Dialogs {
|
||||
await this.smalltalk.alert(title, message);
|
||||
}
|
||||
|
||||
public async confirm(message: string, title = '', options: unknown = {}) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public async confirm(message: string, title = '', options: any = {}) {
|
||||
try {
|
||||
await this.smalltalk.confirm(title, message, options);
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import useCtrlWheelZoom from './useCtrlWheelZoom';
|
||||
|
||||
jest.mock('@joplin/lib/models/Setting', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
incValue: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const dispatchWheel = (options: WheelEventInit) => {
|
||||
document.dispatchEvent(new WheelEvent('wheel', { bubbles: true, ...options }));
|
||||
};
|
||||
|
||||
const dispatchKeyDown = (key: string) => {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
||||
};
|
||||
|
||||
const dispatchKeyUp = (key: string) => {
|
||||
document.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
|
||||
};
|
||||
|
||||
describe('useCtrlWheelZoom', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should zoom when Ctrl key is pressed and wheel is scrolled', () => {
|
||||
renderHook(() => useCtrlWheelZoom());
|
||||
|
||||
dispatchKeyDown('Control');
|
||||
dispatchWheel({ deltaY: -100 });
|
||||
expect(Setting.incValue).toHaveBeenCalledWith('windowContentZoomFactor', 10);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
dispatchWheel({ deltaY: 100 });
|
||||
expect(Setting.incValue).toHaveBeenCalledWith('windowContentZoomFactor', -10);
|
||||
|
||||
dispatchKeyUp('Control');
|
||||
});
|
||||
|
||||
test('should zoom when Meta key is pressed and wheel is scrolled', () => {
|
||||
renderHook(() => useCtrlWheelZoom());
|
||||
|
||||
dispatchKeyDown('Meta');
|
||||
dispatchWheel({ deltaY: -100 });
|
||||
expect(Setting.incValue).toHaveBeenCalledWith('windowContentZoomFactor', 10);
|
||||
|
||||
dispatchKeyUp('Meta');
|
||||
});
|
||||
|
||||
test('should not zoom on wheel without modifier key pressed', () => {
|
||||
renderHook(() => useCtrlWheelZoom());
|
||||
|
||||
dispatchWheel({ deltaY: -100 });
|
||||
expect(Setting.incValue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not zoom when only ctrlKey flag is set on wheel event (trackpad pinch)', () => {
|
||||
// On macOS, trackpad pinch gestures send wheel events with ctrlKey=true
|
||||
// but without actually pressing the Ctrl key
|
||||
renderHook(() => useCtrlWheelZoom());
|
||||
|
||||
dispatchWheel({ deltaY: -100, ctrlKey: true });
|
||||
expect(Setting.incValue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should stop zooming after key is released', () => {
|
||||
renderHook(() => useCtrlWheelZoom());
|
||||
|
||||
dispatchKeyDown('Control');
|
||||
dispatchWheel({ deltaY: -100 });
|
||||
expect(Setting.incValue).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
dispatchKeyUp('Control');
|
||||
dispatchWheel({ deltaY: -100 });
|
||||
expect(Setting.incValue).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const useCtrlWheelZoom = () => {
|
||||
useEffect(() => {
|
||||
// Track whether modifier keys are actually pressed via keyboard events.
|
||||
// This is needed because on macOS, trackpad pinch-to-zoom gestures are
|
||||
// reported as wheel events with ctrlKey=true, even though Ctrl isn't pressed.
|
||||
let ctrlPressed = false;
|
||||
let metaPressed = false;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Control') ctrlPressed = true;
|
||||
if (e.key === 'Meta') metaPressed = true;
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Control') ctrlPressed = false;
|
||||
if (e.key === 'Meta') metaPressed = false;
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
ctrlPressed = false;
|
||||
metaPressed = false;
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (ctrlPressed || metaPressed) {
|
||||
e.preventDefault();
|
||||
Setting.incValue('windowContentZoomFactor', e.deltaY < 0 ? 10 : -10);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
window.addEventListener('blur', handleBlur);
|
||||
document.addEventListener('wheel', handleWheel, { passive: false });
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.removeEventListener('keyup', handleKeyUp);
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
document.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useCtrlWheelZoom;
|
||||
@@ -373,7 +373,7 @@
|
||||
ipc.focus = (event) => {
|
||||
const dummyID = 'joplin-content-focus-dummy';
|
||||
if (! document.getElementById(dummyID)) {
|
||||
const focusDummy = '<div style="width: 0; height: 0; overflow: hidden"><a id="' + dummyID + '" href="#" aria-label="Note viewer top"></a></div>';
|
||||
const focusDummy = '<div style="width: 0; height: 0; overflow: hidden"><a id="' + dummyID + '" href="#">Note viewer top</a></div>';
|
||||
contentElement.insertAdjacentHTML("afterbegin", focusDummy);
|
||||
}
|
||||
const scrollTop = contentElement.scrollTop;
|
||||
@@ -827,37 +827,7 @@
|
||||
}));
|
||||
|
||||
// By default, Chromium inlines body styles (e.g. theme background color) into the clipboard HTML.
|
||||
// Intercept the copy event and write only the selected content with inlined light-theme styles to bypass this behaviour.
|
||||
const clipboardCssProps = [
|
||||
'font-family', 'font-size', 'font-weight', 'font-style',
|
||||
'text-decoration-line', 'line-height', 'color',
|
||||
'background-color',
|
||||
'margin-top', 'margin-bottom', 'margin-left', 'margin-right',
|
||||
'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
||||
'border-top', 'border-bottom', 'border-left', 'border-right',
|
||||
'border-collapse', 'border-radius',
|
||||
'list-style-type',
|
||||
'white-space', 'text-align',
|
||||
'opacity',
|
||||
];
|
||||
|
||||
const lightThemeOverrideCss = [
|
||||
'body, #joplin-container-content, #rendered-md { background-color: transparent !important; color: #32373F !important; }',
|
||||
'html *, html *::before, html *::after { background-color: transparent !important; }',
|
||||
'a { color: #155BDA !important; }',
|
||||
'code, .inline-code, .mce-content-body code { color: rgb(0,0,0) !important; background-color: rgb(243,243,243) !important; border-color: rgb(220,220,220) !important; }',
|
||||
'pre.hljs { background-color: rgb(243,243,243) !important; }',
|
||||
'pre.hljs code { background-color: transparent !important; }',
|
||||
'kbd { color: rgb(0,0,0) !important; background-color: rgb(243,243,243) !important; }',
|
||||
'table, table thead, table tbody, table tr, table td, table th { background-color: transparent !important; color: #32373F !important; }',
|
||||
'table th { background-color: rgb(247,247,247) !important; }',
|
||||
'table:has(thead) tr:nth-child(even) { background-color: rgb(247,247,247) !important; }',
|
||||
'table td, table th { border-color: rgb(220,220,220) !important; }',
|
||||
'blockquote { border-left-color: rgb(220,220,220) !important; opacity: 0.7 !important; }',
|
||||
'h1 { border-bottom-color: #dddddd !important; }',
|
||||
'mark { background-color: #F3B717 !important; color: black !important; }',
|
||||
].join('\n');
|
||||
|
||||
// Intercept the copy event and write only the selected content to bypass this behaviour.
|
||||
document.addEventListener('copy', webviewLib.logEnabledEventHandler(e => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed) return;
|
||||
@@ -867,19 +837,8 @@
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.appendChild(range.cloneContents());
|
||||
|
||||
|
||||
wrapper.querySelectorAll('style').forEach(s => s.remove());
|
||||
|
||||
// The hidden attribute on .joplin-source helps some apps, but many
|
||||
// external editors (Word, Google Docs) ignore hidden/display:none
|
||||
// when pasting clipboard HTML. Remove them explicitly as a fallback.
|
||||
wrapper.querySelectorAll('.joplin-source').forEach(s => s.remove());
|
||||
|
||||
// Remove the accessibility focus dummy link container.
|
||||
const focusDummy = wrapper.querySelector('#joplin-content-focus-dummy');
|
||||
if (focusDummy && focusDummy.parentElement) focusDummy.parentElement.remove();
|
||||
|
||||
|
||||
const inlineTags = new Set(['STRONG', 'EM', 'CODE', 'S', 'DEL', 'INS', 'MARK', 'SUP', 'SUB', 'U', 'SPAN', 'A']);
|
||||
let node = range.commonAncestorContainer;
|
||||
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
||||
@@ -893,35 +852,6 @@
|
||||
node = node.parentElement;
|
||||
}
|
||||
|
||||
const overrideStyle = document.createElement('style');
|
||||
overrideStyle.textContent = lightThemeOverrideCss;
|
||||
document.head.appendChild(overrideStyle);
|
||||
|
||||
const renderedMd = document.getElementById('rendered-md') || contentElement;
|
||||
wrapper.style.cssText = 'position:absolute;left:-9999px;visibility:hidden;pointer-events:none';
|
||||
renderedMd.appendChild(wrapper);
|
||||
|
||||
try {
|
||||
const elements = wrapper.querySelectorAll('*');
|
||||
for (const el of elements) {
|
||||
let cs;
|
||||
try { cs = window.getComputedStyle(el); } catch (_) { continue; }
|
||||
|
||||
const parts = [];
|
||||
for (const prop of clipboardCssProps) {
|
||||
const val = cs.getPropertyValue(prop);
|
||||
if (val) parts.push(`${prop}: ${val}`);
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
el.setAttribute('style', parts.join('; '));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
renderedMd.removeChild(wrapper);
|
||||
overrideStyle.remove();
|
||||
wrapper.style.cssText = '';
|
||||
}
|
||||
|
||||
e.clipboardData.setData('text/html', wrapper.innerHTML);
|
||||
e.clipboardData.setData('text/plain', selection.toString());
|
||||
e.preventDefault();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
@use './base-button.scss';
|
||||
@use './dialog-modal-layer.scss';
|
||||
@use './user-webview-dialog.scss';
|
||||
@use './note-property-box.scss';
|
||||
@use './prompt-dialog.scss';
|
||||
@use './flat-button.scss';
|
||||
@use './link-button.scss';
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.note-property-box {
|
||||
> th, > td {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rdt {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -7,40 +7,26 @@ import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
import setSettingValue from './util/setSettingValue';
|
||||
import { toForwardSlashes } from '@joplin/utils/path';
|
||||
import mockClipboard from './util/mockClipboard';
|
||||
import { ElectronApplication, Page } from '@playwright/test';
|
||||
|
||||
const importAndOpenHtmlExport = async (mainWindow: Page, electronApp: ElectronApplication, noteTitle: string) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
|
||||
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
|
||||
await importedFolder.waitFor();
|
||||
|
||||
// Retry -- focusing the imported-folder may fail in some cases
|
||||
await expect(async () => {
|
||||
await importedFolder.click();
|
||||
|
||||
await mainScreen.noteList.focusContent(electronApp);
|
||||
|
||||
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle(noteTitle);
|
||||
await importedHtmlFileItem.click({ timeout: 300 });
|
||||
}).toPass();
|
||||
|
||||
return { mainScreen };
|
||||
};
|
||||
|
||||
test.describe('markdownEditor', () => {
|
||||
test('editor should render the full content of HTML notes', async ({ mainWindow, electronApp }) => {
|
||||
const { mainScreen } = await importAndOpenHtmlExport(mainWindow, electronApp, 'test-html-file-with-spans');
|
||||
|
||||
const editor = mainScreen.noteEditor.codeMirrorEditor;
|
||||
// Regression test: The <span> should not be hidden by inline Markdown rendering (since this is an HTML note):
|
||||
await expect(editor).toHaveText('<p><span style="margin-left: 100px;">test</span></p>');
|
||||
});
|
||||
|
||||
test('preview pane should render images in HTML notes', async ({ mainWindow, electronApp }) => {
|
||||
const { mainScreen } = await importAndOpenHtmlExport(mainWindow, electronApp, 'test-html-file-with-image');
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.importHtmlDirectory(electronApp, join(__dirname, 'resources', 'html-import'));
|
||||
const importedFolder = mainScreen.sidebar.container.getByText('html-import');
|
||||
await importedFolder.waitFor();
|
||||
|
||||
// Retry -- focusing the imported-folder may fail in some cases
|
||||
await expect(async () => {
|
||||
await importedFolder.click();
|
||||
|
||||
await mainScreen.noteList.focusContent(electronApp);
|
||||
|
||||
const importedHtmlFileItem = mainScreen.noteList.getNoteItemByTitle('test-html-file-with-image');
|
||||
await importedHtmlFileItem.click({ timeout: 300 });
|
||||
}).toPass();
|
||||
|
||||
const viewerFrame = mainScreen.noteEditor.getNoteViewerFrameLocator();
|
||||
// Should render headers
|
||||
@@ -446,9 +432,7 @@ test.describe('markdownEditor', () => {
|
||||
});
|
||||
|
||||
expect(clipboardHtml).toContain('hello');
|
||||
// Dark theme background (#1D2024) must not leak into clipboard
|
||||
expect(clipboardHtml).not.toMatch(/1D2024/i);
|
||||
expect(clipboardHtml).toContain('<strong');
|
||||
expect(clipboardHtml).toMatch(/font-weight/i);
|
||||
expect(clipboardHtml).not.toMatch(/background-color\s*:/i);
|
||||
expect(clipboardHtml).toContain('<strong>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,29 +3,7 @@ import MainScreen from './models/MainScreen';
|
||||
import NoteEditorScreen from './models/NoteEditorScreen';
|
||||
|
||||
test.describe('multiWindow', () => {
|
||||
// Disabled: Playwright's page.close() triggers a different code path than
|
||||
// a user closing the window, causing the test to be unreliable.
|
||||
// The fix works correctly in manual testing (see https://github.com/laurent22/joplin/issues/14628).
|
||||
test.fixme('should not crash when closing a secondary window', async ({ mainWindow, electronApp }) => {
|
||||
const mainPage = await new MainScreen(mainWindow).setup();
|
||||
await mainPage.createNewNote('Test');
|
||||
|
||||
const window = await mainPage.openNewWindow(electronApp);
|
||||
|
||||
// Should load successfully
|
||||
const screen = new NoteEditorScreen(window);
|
||||
await screen.waitFor();
|
||||
|
||||
// Close the secondary window
|
||||
await window.close();
|
||||
|
||||
// Wait for the Portal cleanup to complete before checking main window stability
|
||||
await mainWindow.waitForTimeout(2000);
|
||||
|
||||
// Main window should remain stable — no white screen or renderer crash
|
||||
await expect(await mainPage.noteEditor.contentLocator()).toBeVisible();
|
||||
});
|
||||
|
||||
// Disabled: This test often hangs when closing secondary windows (see https://github.com/laurent22/joplin/issues/14628):
|
||||
test.fixme('should support quickly creating, then closing secondary windows', async ({ mainWindow, electronApp }) => {
|
||||
const mainPage = await new MainScreen(mainWindow).setup();
|
||||
await mainPage.createNewNote('Test');
|
||||
@@ -50,3 +28,4 @@ test.describe('multiWindow', () => {
|
||||
await expect(await mainPage.noteEditor.contentLocator()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -101,35 +101,6 @@ test.describe('noteList', () => {
|
||||
await expect(testNoteItem).toBeVisible();
|
||||
});
|
||||
|
||||
test('should remain focused after deleting a note to the trash', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('test note 1');
|
||||
await mainScreen.createNewNote('test note 2');
|
||||
await mainScreen.createNewNote('test note 3');
|
||||
|
||||
const noteList = mainScreen.noteList;
|
||||
await noteList.sortByTitle(electronApp);
|
||||
await noteList.focusContent(electronApp);
|
||||
|
||||
// The most-recently created note should be selected
|
||||
await noteList.expectNoteToBeSelected('test note 3');
|
||||
|
||||
// All three notes should be visible
|
||||
const getNote = (i: number) => noteList.getNoteItemByTitle(`test note ${i}`);
|
||||
await expect(getNote(1)).toBeVisible();
|
||||
await expect(getNote(2)).toBeVisible();
|
||||
await expect(getNote(3)).toBeVisible();
|
||||
|
||||
await getNote(3).press('Delete');
|
||||
await expect(getNote(3)).not.toBeVisible();
|
||||
|
||||
// Pressing the up arrow should change the selection
|
||||
// (Regression test for https://github.com/laurent22/joplin/issues/10753)
|
||||
await noteList.expectNoteToBeSelected('test note 2');
|
||||
await noteList.container.press('ArrowUp');
|
||||
await noteList.expectNoteToBeSelected('test note 1');
|
||||
});
|
||||
|
||||
test('arrow keys should navigate the note list', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
<p><span style="margin-left: 100px;">test</span></p>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import { Second } from '@joplin/utils/time';
|
||||
|
||||
test.describe('sidebar', () => {
|
||||
test('should be able to create new folders', async ({ mainWindow }) => {
|
||||
@@ -45,54 +44,6 @@ test.describe('sidebar', () => {
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/laurent22/joplin/issues/15029
|
||||
test('should remain focused when navigating with the arrow keys', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
// Build the folder hierarchy: Navigating upwards through the list
|
||||
// should transition from a notebook with more notes to a notebook with
|
||||
// fewer notes.
|
||||
const folderAHeader = await sidebar.createNewFolder('Folder A');
|
||||
await mainScreen.createNewNote('Test');
|
||||
await expect(folderAHeader).toBeVisible();
|
||||
const folderBHeader = await sidebar.createNewFolder('Folder B');
|
||||
await mainScreen.createNewNote('Test 2');
|
||||
await mainScreen.createNewNote('Test 3');
|
||||
const folderCHeader = await sidebar.createNewFolder('Folder C');
|
||||
const folderDHeader = await sidebar.createNewFolder('Folder D');
|
||||
|
||||
await folderBHeader.dragTo(folderAHeader);
|
||||
await folderCHeader.dragTo(folderAHeader);
|
||||
|
||||
// Should have the correct initial state
|
||||
await sidebar.forceUpdateSorting(electronApp);
|
||||
await sidebar.expectToHaveDepths([
|
||||
[folderAHeader, 2],
|
||||
[folderBHeader, 3],
|
||||
[folderCHeader, 3],
|
||||
[folderDHeader, 2],
|
||||
]);
|
||||
|
||||
const assertFocused = async (title: RegExp) => {
|
||||
await expect(mainWindow.locator(':focus')).toHaveText(title);
|
||||
// Pause to help check that focus is stable. This is present to help this test more reliably detect
|
||||
// timing-related issues.
|
||||
await mainWindow.waitForTimeout(Second);
|
||||
await expect(mainWindow.locator(':focus')).toHaveText(title);
|
||||
};
|
||||
|
||||
await folderDHeader.click();
|
||||
|
||||
// Focus should remain on the correct folder header while navigating
|
||||
await mainWindow.keyboard.press('ArrowUp');
|
||||
await assertFocused(/^Folder C/);
|
||||
await mainWindow.keyboard.press('ArrowUp');
|
||||
await assertFocused(/^Folder B/);
|
||||
await mainWindow.keyboard.press('ArrowUp');
|
||||
await assertFocused(/^Folder A/);
|
||||
});
|
||||
|
||||
test('should allow changing the focused folder by pressing the first character of the title', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { Page } from '@playwright/test';
|
||||
import SettingsScreen from './models/SettingsScreen';
|
||||
|
||||
const createScanner = (page: Page) => {
|
||||
return new AxeBuilder({ page })
|
||||
@@ -39,24 +38,25 @@ const expectNoViolations = async (page: Page) => {
|
||||
};
|
||||
|
||||
test.describe('wcag', () => {
|
||||
for (const tabName of ['General', 'Plugins']) {
|
||||
test(`should not detect significant issues in the settings screen ${tabName} tab`, async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.openSettings(electronApp);
|
||||
|
||||
// Should be on the settings screen
|
||||
const settingsScreen = new SettingsScreen(mainWindow);
|
||||
await settingsScreen.waitFor();
|
||||
|
||||
const tabLocator = settingsScreen.getTabLocator(tabName);
|
||||
await tabLocator.click();
|
||||
await expect(tabLocator).toBeFocused();
|
||||
|
||||
await expectNoViolations(mainWindow);
|
||||
});
|
||||
}
|
||||
// Disabled due to random failure in CI:
|
||||
// for (const tabName of ['General', 'Plugins']) {
|
||||
// test(`should not detect significant issues in the settings screen ${tabName} tab`, async ({ electronApp, mainWindow }) => {
|
||||
// const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
// await mainScreen.waitFor();
|
||||
//
|
||||
// await mainScreen.openSettings(electronApp);
|
||||
//
|
||||
// // Should be on the settings screen
|
||||
// const settingsScreen = new SettingsScreen(mainWindow);
|
||||
// await settingsScreen.waitFor();
|
||||
//
|
||||
// const tabLocator = settingsScreen.getTabLocator(tabName);
|
||||
// await tabLocator.click();
|
||||
// await expect(tabLocator).toBeFocused();
|
||||
//
|
||||
// await expectNoViolations(mainWindow);
|
||||
// });
|
||||
// }
|
||||
|
||||
test('should not detect significant issues in the main screen with an open note', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
@@ -82,17 +82,6 @@ test.describe('wcag', () => {
|
||||
await expectNoViolations(mainWindow);
|
||||
});
|
||||
|
||||
test('should not detect significant issues in the note properties screen', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('Test');
|
||||
await mainScreen.goToAnything.runCommand(electronApp, 'showNoteProperties');
|
||||
|
||||
const header = mainScreen.dialog.locator('h1');
|
||||
await expect(header).toBeVisible();
|
||||
|
||||
await expectNoViolations(mainWindow);
|
||||
});
|
||||
|
||||
test('should not detect significant issues in the change app layout screen', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.changeLayoutScreen.open(electronApp);
|
||||
|
||||
@@ -106,6 +106,10 @@ a {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.note-property-box .rdt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.help-tooltip {
|
||||
font-family: sans-serif;
|
||||
max-width: 200px;
|
||||
@@ -286,18 +290,18 @@ Component-specific classes
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.master-password-dialog .dialog-root, .enable-encryption-dialog .dialog-root {
|
||||
.master-password-dialog .dialog-root {
|
||||
min-width: 500px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.master-password-dialog .dialog-content, .enable-encryption-dialog .dialog-content {
|
||||
.master-password-dialog .dialog-content {
|
||||
background-color: var(--joplin-background-color3);
|
||||
padding: 1em;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.master-password-dialog .current-password-wrapper, .enable-encryption-dialog .current-password-wrapper {
|
||||
.master-password-dialog .current-password-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user