diff --git a/.eslintignore b/.eslintignore index 76bf10778c..d67c773d9b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.gitignore b/.gitignore index f2cf8d2212..9966858534 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 26884bd340..bf39c3fc23 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -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) { diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.ts index 0b8c968718..ccec7080bf 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.ts +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.ts @@ -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(); }); } diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts index f238c44bd8..85baceccb2 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts @@ -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, diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.ts new file mode 100644 index 0000000000..9437ff7266 --- /dev/null +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.ts @@ -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', + }; +}; diff --git a/packages/app-desktop/gui/menuCommandNames.ts b/packages/app-desktop/gui/menuCommandNames.ts index 4489ae94dd..5dd9e81fe2 100644 --- a/packages/app-desktop/gui/menuCommandNames.ts +++ b/packages/app-desktop/gui/menuCommandNames.ts @@ -59,6 +59,7 @@ export default function() { 'editor.sortSelectedLines', 'editor.swapLineUp', 'editor.swapLineDown', + 'linkToNote', 'exportDeletionLog', 'toggleSafeMode', 'showShareNoteDialog', diff --git a/packages/app-desktop/plugins/GotoAnything.tsx b/packages/app-desktop/plugins/GotoAnything.tsx index d875702603..08d6137a95 100644 --- a/packages/app-desktop/plugins/GotoAnything.tsx +++ b/packages/app-desktop/plugins/GotoAnything.tsx @@ -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 { 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 { 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 { // 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 { aria-posinset={index + 1} >
- {fragmentComp} - {pathComp} + {this.mode_ === Mode.TitleOnly ? null : fragmentComp} + {this.mode_ === Mode.TitleOnly ? null : pathComp} ); } @@ -668,6 +709,14 @@ class DialogComponent extends React.PureComponent { ); } + 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 { 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.')} + >{this.helpText()} ); return ( diff --git a/packages/lib/services/KeymapService.ts b/packages/lib/services/KeymapService.ts index 220863561d..d5d62bb441 100644 --- a/packages/lib/services/KeymapService.ts +++ b/packages/lib/services/KeymapService.ts @@ -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' }, ], }; diff --git a/readme/apps/link_to_note.md b/readme/apps/link_to_note.md new file mode 100644 index 0000000000..8c0798344b --- /dev/null +++ b/readme/apps/link_to_note.md @@ -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 Enter when done. + +This will create a new link and insert it into your current note. \ No newline at end of file