Desktop: Add dialog to select a note and link to it (#11891)

pull/11899/head
Laurent Cozic 2025-02-27 18:24:02 +00:00 committed by GitHub
parent 9cbd1b855c
commit 8bdb6c5d72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 129 additions and 10 deletions

View File

@ -436,6 +436,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js

1
.gitignore vendored
View File

@ -411,6 +411,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js

View File

@ -999,6 +999,7 @@ function useMenu(props: Props) {
rootMenus.go.submenu.push(menuItemDic.gotoAnything);
rootMenus.tools.submenu.push(menuItemDic.commandPalette);
rootMenus.tools.submenu.push(menuItemDic.linkToNote);
rootMenus.tools.submenu.push(menuItemDic.openMasterPasswordDialog);
for (const view of props.pluginMenuItems) {

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { GotoAnythingUserData, Mode, UserDataCallbackReject, UserDataCallbackResolve } from '../../../plugins/GotoAnything';
const PluginManager = require('@joplin/lib/services/PluginManager');
export enum UiType {
@ -8,6 +9,10 @@ export enum UiType {
ControlledApi = 'controlledApi',
}
export interface GotoAnythingOptions {
mode?: Mode;
}
export const declaration: CommandDeclaration = {
name: 'gotoAnything',
label: () => _('Goto Anything...'),
@ -24,19 +29,26 @@ function menuItemById(id: string) {
// calling the click() handler.
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext, uiType: UiType = UiType.GotoAnything) => {
execute: async (_context: CommandContext, uiType: UiType = UiType.GotoAnything, options: GotoAnythingOptions = null) => {
options = {
mode: Mode.Default,
...options,
};
if (uiType === UiType.GotoAnything) {
menuItemById('gotoAnything').click();
} else if (uiType === UiType.CommandPalette) {
menuItemById('commandPalette').click();
} else if (uiType === UiType.ControlledApi) {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
return new Promise((resolve: Function, reject: Function) => {
return new Promise((resolve: UserDataCallbackResolve, reject: UserDataCallbackReject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const menuItem = PluginManager.instance().menuItems().find((i: any) => i.id === 'controlledApi');
menuItem.userData = {
const userData: GotoAnythingUserData = {
callback: { resolve, reject },
mode: options.mode,
};
menuItem.userData = userData;
menuItem.click();
});
}

View File

@ -8,6 +8,7 @@ import * as exportPdf from './exportPdf';
import * as gotoAnything from './gotoAnything';
import * as hideModalMessage from './hideModalMessage';
import * as leaveSharedFolder from './leaveSharedFolder';
import * as linkToNote from './linkToNote';
import * as moveToFolder from './moveToFolder';
import * as newFolder from './newFolder';
import * as newNote from './newNote';
@ -56,6 +57,7 @@ const index: any[] = [
gotoAnything,
hideModalMessage,
leaveSharedFolder,
linkToNote,
moveToFolder,
newFolder,
newNote,

View File

@ -0,0 +1,37 @@
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { Mode } from '../../../plugins/GotoAnything';
import { GotoAnythingOptions, UiType } from './gotoAnything';
import { ModelType } from '@joplin/lib/BaseModel';
import Logger from '@joplin/utils/Logger';
import markdownUtils from '@joplin/lib/markdownUtils';
const logger = Logger.create('linkToNote');
export const declaration: CommandDeclaration = {
name: 'linkToNote',
label: () => _('Link to note...'),
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext) => {
const options: GotoAnythingOptions = {
mode: Mode.TitleOnly,
};
const result = await CommandService.instance().execute('gotoAnything', UiType.ControlledApi, options);
if (!result) return result;
if (result.type !== ModelType.Note) {
logger.warn('Retrieved item is not a note:', result);
return null;
}
const link = `[${markdownUtils.escapeTitleText(result.item.title)}](:/${markdownUtils.escapeLinkUrl(result.item.id)})`;
await CommandService.instance().execute('insertText', link);
return result;
},
enabledCondition: 'markdownEditorPaneVisible || richTextEditorVisible',
};
};

View File

@ -59,6 +59,7 @@ export default function() {
'editor.sortSelectedLines',
'editor.swapLineUp',
'editor.swapLineDown',
'linkToNote',
'exportDeletionLog',
'toggleSafeMode',
'showShareNoteDialog',

View File

@ -40,6 +40,39 @@ interface GotoAnythingSearchResult {
item_type?: ModelType;
}
// GotoAnything supports several modes:
//
// - Default: Search in note title, body. Can search for folders, tags, etc. This is the full
// featured GotoAnything.
//
// - TitleOnly: Search in note titles only.
//
// These different modes can be set from the `gotoAnything` command.
export enum Mode {
Default = 0,
TitleOnly,
}
export interface UserDataCallbackEvent {
type: ModelType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
item: any;
}
export type UserDataCallbackResolve = (event: UserDataCallbackEvent)=> void;
export type UserDataCallbackReject = (error: Error)=> void;
export interface UserDataCallback {
resolve: UserDataCallbackResolve;
reject: UserDataCallbackReject;
}
export interface GotoAnythingUserData {
startString?: string;
mode?: Mode;
callback?: UserDataCallback;
}
interface Props {
themeId: number;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@ -47,8 +80,7 @@ interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
folders: any[];
showCompletedTodos: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
userData: any;
userData: GotoAnythingUserData;
}
interface State {
@ -131,8 +163,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
private itemListRef: any;
private listUpdateQueue_: AsyncActionQueue;
private markupToHtml_: MarkupToHtml;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private userCallback_: any = null;
private userCallback_: UserDataCallback|null = null;
private mode_: Mode;
public constructor(props: Props) {
super(props);
@ -142,6 +174,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
this.userCallback_ = props?.userData?.callback;
this.listUpdateQueue_ = new AsyncActionQueue(100);
this.mode_ = props?.userData?.mode ? props.userData.mode : Mode.Default;
this.state = {
query: startString,
results: [],
@ -341,6 +375,13 @@ class DialogComponent extends React.PureComponent<Props, State> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
resultsInBody = !!results.find((row: any) => row.fields.includes('body'));
if (this.mode_ === Mode.TitleOnly) {
resultsInBody = false;
results = results.filter(r => {
return r.fields.includes('title');
});
}
const resourceIds = results.filter(r => r.item_type === ModelType.Resource).map(r => r.item_id);
const resources = await Resource.resourceOcrTextsByIds(resourceIds);
@ -584,8 +625,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
aria-posinset={index + 1}
>
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
{fragmentComp}
{pathComp}
{this.mode_ === Mode.TitleOnly ? null : fragmentComp}
{this.mode_ === Mode.TitleOnly ? null : pathComp}
</div>
);
}
@ -668,6 +709,14 @@ class DialogComponent extends React.PureComponent<Props, State> {
);
}
private helpText() {
if (this.mode_ === Mode.TitleOnly) {
return _('Type a note title to search for it.');
} else {
return _('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.');
}
}
public render() {
const style = this.style();
const helpTextId = 'goto-anything-help-text';
@ -678,7 +727,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
id={helpTextId}
style={style.help}
hidden={!this.state.showHelp}
>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.')}</div>
>{this.helpText()}</div>
);
return (

View File

@ -64,6 +64,7 @@ const defaultKeymapItems = {
{ accelerator: 'Option+Cmd+Backspace', command: 'permanentlyDeleteNote' },
{ accelerator: 'Option+Cmd+N', command: 'openNoteInNewWindow' },
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
{ accelerator: 'Shift+Option+L', command: 'linkToNote' },
],
default: [
{ accelerator: 'Ctrl+N', command: 'newNote' },
@ -114,6 +115,7 @@ const defaultKeymapItems = {
{ accelerator: 'Ctrl+Alt+3', command: 'switchProfile3' },
{ accelerator: 'Ctrl+Alt+N', command: 'openNoteInNewWindow' },
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
{ accelerator: 'Shift+Alt+L', command: 'linkToNote' },
],
};

View File

@ -0,0 +1,13 @@
# Link to note
To create a link to a note, you have two options:
## Create a Markdown link
Simply create the link in Markdown, as described in the [Markdown guide](https://joplinapp.org/help/apps/markdown/#links-to-other-notes).
## Use the "Link to note" dialog
An easier way is to use the "Link to note" dialog - to do so open the dialog from **Tools => Link to note...**. Then type the note you would like to link to and press <kbd>Enter</kbd> when done.
This will create a new link and insert it into your current note.