mirror of https://github.com/laurent22/joplin.git
Desktop: Add dialog to select a note and link to it (#11891)
parent
9cbd1b855c
commit
8bdb6c5d72
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.swapLineUp',
|
||||
'editor.swapLineDown',
|
||||
'linkToNote',
|
||||
'exportDeletionLog',
|
||||
'toggleSafeMode',
|
||||
'showShareNoteDialog',
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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' },
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue