You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-23 22:36:32 +02:00
Desktop: Add dialog to select a note and link to it (#11891)
This commit is contained in:
@@ -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/hideModalMessage.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.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/moveToFolder.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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/hideModalMessage.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.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/moveToFolder.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js
|
||||||
|
|||||||
@@ -999,6 +999,7 @@ function useMenu(props: Props) {
|
|||||||
|
|
||||||
rootMenus.go.submenu.push(menuItemDic.gotoAnything);
|
rootMenus.go.submenu.push(menuItemDic.gotoAnything);
|
||||||
rootMenus.tools.submenu.push(menuItemDic.commandPalette);
|
rootMenus.tools.submenu.push(menuItemDic.commandPalette);
|
||||||
|
rootMenus.tools.submenu.push(menuItemDic.linkToNote);
|
||||||
rootMenus.tools.submenu.push(menuItemDic.openMasterPasswordDialog);
|
rootMenus.tools.submenu.push(menuItemDic.openMasterPasswordDialog);
|
||||||
|
|
||||||
for (const view of props.pluginMenuItems) {
|
for (const view of props.pluginMenuItems) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { GotoAnythingUserData, Mode, UserDataCallbackReject, UserDataCallbackResolve } from '../../../plugins/GotoAnything';
|
||||||
const PluginManager = require('@joplin/lib/services/PluginManager');
|
const PluginManager = require('@joplin/lib/services/PluginManager');
|
||||||
|
|
||||||
export enum UiType {
|
export enum UiType {
|
||||||
@@ -8,6 +9,10 @@ export enum UiType {
|
|||||||
ControlledApi = 'controlledApi',
|
ControlledApi = 'controlledApi',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GotoAnythingOptions {
|
||||||
|
mode?: Mode;
|
||||||
|
}
|
||||||
|
|
||||||
export const declaration: CommandDeclaration = {
|
export const declaration: CommandDeclaration = {
|
||||||
name: 'gotoAnything',
|
name: 'gotoAnything',
|
||||||
label: () => _('Goto Anything...'),
|
label: () => _('Goto Anything...'),
|
||||||
@@ -24,19 +29,26 @@ function menuItemById(id: string) {
|
|||||||
// calling the click() handler.
|
// calling the click() handler.
|
||||||
export const runtime = (): CommandRuntime => {
|
export const runtime = (): CommandRuntime => {
|
||||||
return {
|
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) {
|
if (uiType === UiType.GotoAnything) {
|
||||||
menuItemById('gotoAnything').click();
|
menuItemById('gotoAnything').click();
|
||||||
} else if (uiType === UiType.CommandPalette) {
|
} else if (uiType === UiType.CommandPalette) {
|
||||||
menuItemById('commandPalette').click();
|
menuItemById('commandPalette').click();
|
||||||
} else if (uiType === UiType.ControlledApi) {
|
} else if (uiType === UiType.ControlledApi) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// 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
|
// 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');
|
const menuItem = PluginManager.instance().menuItems().find((i: any) => i.id === 'controlledApi');
|
||||||
menuItem.userData = {
|
const userData: GotoAnythingUserData = {
|
||||||
callback: { resolve, reject },
|
callback: { resolve, reject },
|
||||||
|
mode: options.mode,
|
||||||
};
|
};
|
||||||
|
menuItem.userData = userData;
|
||||||
menuItem.click();
|
menuItem.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as exportPdf from './exportPdf';
|
|||||||
import * as gotoAnything from './gotoAnything';
|
import * as gotoAnything from './gotoAnything';
|
||||||
import * as hideModalMessage from './hideModalMessage';
|
import * as hideModalMessage from './hideModalMessage';
|
||||||
import * as leaveSharedFolder from './leaveSharedFolder';
|
import * as leaveSharedFolder from './leaveSharedFolder';
|
||||||
|
import * as linkToNote from './linkToNote';
|
||||||
import * as moveToFolder from './moveToFolder';
|
import * as moveToFolder from './moveToFolder';
|
||||||
import * as newFolder from './newFolder';
|
import * as newFolder from './newFolder';
|
||||||
import * as newNote from './newNote';
|
import * as newNote from './newNote';
|
||||||
@@ -56,6 +57,7 @@ const index: any[] = [
|
|||||||
gotoAnything,
|
gotoAnything,
|
||||||
hideModalMessage,
|
hideModalMessage,
|
||||||
leaveSharedFolder,
|
leaveSharedFolder,
|
||||||
|
linkToNote,
|
||||||
moveToFolder,
|
moveToFolder,
|
||||||
newFolder,
|
newFolder,
|
||||||
newNote,
|
newNote,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -59,6 +59,7 @@ export default function() {
|
|||||||
'editor.sortSelectedLines',
|
'editor.sortSelectedLines',
|
||||||
'editor.swapLineUp',
|
'editor.swapLineUp',
|
||||||
'editor.swapLineDown',
|
'editor.swapLineDown',
|
||||||
|
'linkToNote',
|
||||||
'exportDeletionLog',
|
'exportDeletionLog',
|
||||||
'toggleSafeMode',
|
'toggleSafeMode',
|
||||||
'showShareNoteDialog',
|
'showShareNoteDialog',
|
||||||
|
|||||||
@@ -40,6 +40,39 @@ interface GotoAnythingSearchResult {
|
|||||||
item_type?: ModelType;
|
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 {
|
interface Props {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
folders: any[];
|
folders: any[];
|
||||||
showCompletedTodos: boolean;
|
showCompletedTodos: boolean;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
userData: GotoAnythingUserData;
|
||||||
userData: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@@ -131,8 +163,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
|||||||
private itemListRef: any;
|
private itemListRef: any;
|
||||||
private listUpdateQueue_: AsyncActionQueue;
|
private listUpdateQueue_: AsyncActionQueue;
|
||||||
private markupToHtml_: MarkupToHtml;
|
private markupToHtml_: MarkupToHtml;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
private userCallback_: UserDataCallback|null = null;
|
||||||
private userCallback_: any = null;
|
private mode_: Mode;
|
||||||
|
|
||||||
public constructor(props: Props) {
|
public constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -142,6 +174,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
|||||||
this.userCallback_ = props?.userData?.callback;
|
this.userCallback_ = props?.userData?.callback;
|
||||||
this.listUpdateQueue_ = new AsyncActionQueue(100);
|
this.listUpdateQueue_ = new AsyncActionQueue(100);
|
||||||
|
|
||||||
|
this.mode_ = props?.userData?.mode ? props.userData.mode : Mode.Default;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
query: startString,
|
query: startString,
|
||||||
results: [],
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
resultsInBody = !!results.find((row: any) => row.fields.includes('body'));
|
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 resourceIds = results.filter(r => r.item_type === ModelType.Resource).map(r => r.item_id);
|
||||||
const resources = await Resource.resourceOcrTextsByIds(resourceIds);
|
const resources = await Resource.resourceOcrTextsByIds(resourceIds);
|
||||||
|
|
||||||
@@ -584,8 +625,8 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
|||||||
aria-posinset={index + 1}
|
aria-posinset={index + 1}
|
||||||
>
|
>
|
||||||
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
|
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
|
||||||
{fragmentComp}
|
{this.mode_ === Mode.TitleOnly ? null : fragmentComp}
|
||||||
{pathComp}
|
{this.mode_ === Mode.TitleOnly ? null : pathComp}
|
||||||
</div>
|
</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() {
|
public render() {
|
||||||
const style = this.style();
|
const style = this.style();
|
||||||
const helpTextId = 'goto-anything-help-text';
|
const helpTextId = 'goto-anything-help-text';
|
||||||
@@ -678,7 +727,7 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
|||||||
id={helpTextId}
|
id={helpTextId}
|
||||||
style={style.help}
|
style={style.help}
|
||||||
hidden={!this.state.showHelp}
|
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 (
|
return (
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const defaultKeymapItems = {
|
|||||||
{ accelerator: 'Option+Cmd+Backspace', command: 'permanentlyDeleteNote' },
|
{ accelerator: 'Option+Cmd+Backspace', command: 'permanentlyDeleteNote' },
|
||||||
{ accelerator: 'Option+Cmd+N', command: 'openNoteInNewWindow' },
|
{ accelerator: 'Option+Cmd+N', command: 'openNoteInNewWindow' },
|
||||||
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
|
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
|
||||||
|
{ accelerator: 'Shift+Option+L', command: 'linkToNote' },
|
||||||
],
|
],
|
||||||
default: [
|
default: [
|
||||||
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
||||||
@@ -114,6 +115,7 @@ const defaultKeymapItems = {
|
|||||||
{ accelerator: 'Ctrl+Alt+3', command: 'switchProfile3' },
|
{ accelerator: 'Ctrl+Alt+3', command: 'switchProfile3' },
|
||||||
{ accelerator: 'Ctrl+Alt+N', command: 'openNoteInNewWindow' },
|
{ accelerator: 'Ctrl+Alt+N', command: 'openNoteInNewWindow' },
|
||||||
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
|
{ accelerator: 'Ctrl+M', command: 'toggleTabMovesFocus' },
|
||||||
|
{ accelerator: 'Shift+Alt+L', command: 'linkToNote' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
13
readme/apps/link_to_note.md
Normal file
13
readme/apps/link_to_note.md
Normal 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.
|
||||||
Reference in New Issue
Block a user