1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-04-18 19:42:23 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Laurent Cozic 07f8fe3966 update 2026-03-18 14:45:48 +00:00
404 changed files with 3182 additions and 12904 deletions
+16 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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'
+4
View File
@@ -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
+1 -1
View File
@@ -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]+: .+"
+1 -1
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+2 -2
View File
@@ -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));
+2 -2
View File
@@ -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 = '';
-19
View File
@@ -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;
+3 -3
View File
@@ -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++) {
+2 -2
View File
@@ -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));
+2 -2
View File
@@ -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);
+3 -3
View File
@@ -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);
+2 -2
View File
@@ -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);
+2 -2
View File
@@ -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));
+2 -2
View File
@@ -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));
}
+4 -4
View File
@@ -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 -2
View File
@@ -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 -2
View File
@@ -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;
+2 -2
View File
@@ -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++) {
+2 -2
View File
@@ -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_) {
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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="$$&#10;" data-joplin-source-close="&#10;$$&#10;">';
const opening = '<pre class="joplin-source" data-joplin-language="katex" data-joplin-source-open="$$&#10;" data-joplin-source-close="&#10;$$&#10;">';
const closing = '</pre>';
// Remove any single leading and trailing newlines, those are included in data-joplin-source-open
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="joplin-editable joplin-abc-notation">
<pre class="joplin-source" hidden data-abc-options="{&quot;responsive&quot;:&quot;resize&quot;}" data-joplin-language="abc" data-joplin-source-open="```abc&#10;" data-joplin-source-close="&#10;```&#10;">{responsive:'resize'}
<pre class="joplin-source" data-abc-options="{&quot;responsive&quot;:&quot;resize&quot;}" data-joplin-language="abc" data-joplin-source-open="```abc&#10;" data-joplin-source-close="&#10;```&#10;">{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&#10;" data-joplin-source-close="&#10;```">function() {
<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="javascript" data-joplin-source-open="```javascript&#10;" data-joplin-source-close="&#10;```">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">&#x27;bonjour&#x27;</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(&quot;https://www.youtube.com/watch?v=iJqe9pC-z-Y&quot;, { resourceId: &quot;&quot; }); 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="&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;" data-joplin-source-open="```&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;&#10;" data-joplin-source-close="&#10;```">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="&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;" data-joplin-source-open="```&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;&#10;" data-joplin-source-close="&#10;```">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&#10;" data-joplin-source-close="&#10;```">&lt;a href=&quot;#&quot; onclick=&quot;leavethisalone&quot;&gt;testing fence&lt;/a&gt;</pre><pre class="hljs"><code><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;#&quot;</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">&quot;leavethisalone&quot;</span>&gt;</span>testing fence<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span></code></pre></div>
<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="html" data-joplin-source-open="```html&#10;" data-joplin-source-close="&#10;```">&lt;a href=&quot;#&quot; onclick=&quot;leavethisalone&quot;&gt;testing fence&lt;/a&gt;</pre><pre class="hljs"><code><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;#&quot;</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">&quot;leavethisalone&quot;</span>&gt;</span>testing fence<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span></code></pre></div>
+1 -5
View File
@@ -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>;
+57 -110
View File
@@ -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) {
+4 -2
View File
@@ -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 {
+18 -8
View File
@@ -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());
+14 -61
View File
@@ -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());
}
}
}
+2 -2
View File
@@ -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 {
+2 -1
View File
@@ -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>
);
};
+2 -1
View File
@@ -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 {
+7 -4
View File
@@ -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;
+5 -3
View File
@@ -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 })}>
-7
View File
@@ -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);
-11
View File
@@ -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: {
+2
View File
@@ -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');
+8 -10
View File
@@ -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()}
+19 -10
View File
@@ -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();
}
}
}
+2 -1
View File
@@ -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,
};
+1 -45
View File
@@ -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
+42 -40
View File
@@ -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;
+2 -1
View File
@@ -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;
}
}
+6 -8
View File
@@ -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);
});
+2 -1
View File
@@ -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 +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);
+7 -3
View File
@@ -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