mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-10 18:39:22 +08:00
refactor(client/ts): use filtered generics for context menu commands
This commit is contained in:
parent
19652fbbce
commit
b01725101d
@ -18,6 +18,8 @@ import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
||||
import { Node } from "../services/tree.js";
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
@ -31,11 +33,28 @@ interface BeforeUploadListener extends Component {
|
||||
beforeUnloadEvent(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for the data/arguments for a given command (see {@link CommandMappings}).
|
||||
*/
|
||||
interface CommandData {
|
||||
ntxId?: string;
|
||||
}
|
||||
|
||||
type CommandMappings = {
|
||||
/**
|
||||
* Represents a set of commands that are triggered from the context menu, providing information such as the selected note.
|
||||
*/
|
||||
export interface ContextMenuCommandData extends CommandData {
|
||||
node: Node;
|
||||
notePath: string;
|
||||
noteId?: string;
|
||||
selectedOrActiveBranchIds: any; // TODO: Remove any once type is defined
|
||||
selectedOrActiveNoteIds: any; // TODO: Remove any once type is defined
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys represent the different commands that can be triggered via {@link AppContext#triggerCommand} (first argument), and the values represent the data or arguments definition of the given command. All data for commands must extend {@link CommandData}.
|
||||
*/
|
||||
export type CommandMappings = {
|
||||
"api-log-messages": CommandData;
|
||||
focusOnDetail: Required<CommandData>;
|
||||
searchNotes: CommandData & {
|
||||
@ -73,12 +92,42 @@ type CommandMappings = {
|
||||
openNoteInNewTab: CommandData;
|
||||
openNoteInNewSplit: CommandData;
|
||||
openNoteInNewWindow: CommandData;
|
||||
openNoteInSplit: CommandData;
|
||||
openInTab: CommandData;
|
||||
insertNoteAfter: CommandData;
|
||||
insertChildNote: CommandData;
|
||||
convertNoteToAttachment: CommandData;
|
||||
copyNotePathToClipboard: CommandData;
|
||||
|
||||
openInTab: ContextMenuCommandData;
|
||||
openNoteInSplit: ContextMenuCommandData;
|
||||
toggleNoteHoisting: ContextMenuCommandData;
|
||||
insertNoteAfter: ContextMenuCommandData;
|
||||
insertChildNote: ContextMenuCommandData;
|
||||
protectSubtree: ContextMenuCommandData;
|
||||
unprotectSubtree: ContextMenuCommandData;
|
||||
openBulkActionsDialog: ContextMenuCommandData;
|
||||
editBranchPrefix: ContextMenuCommandData;
|
||||
convertNoteToAttachment: ContextMenuCommandData;
|
||||
duplicateSubtree: ContextMenuCommandData;
|
||||
expandSubtree: ContextMenuCommandData;
|
||||
collapseSubtree: ContextMenuCommandData;
|
||||
sortChildNotes: ContextMenuCommandData;
|
||||
copyNotePathToClipboard: ContextMenuCommandData;
|
||||
recentChangesInSubtree: ContextMenuCommandData;
|
||||
cutNotesToClipboard: ContextMenuCommandData;
|
||||
copyNotesToClipboard: ContextMenuCommandData;
|
||||
pasteNotesFromClipboard: ContextMenuCommandData;
|
||||
pasteNotesAfterFromClipboard: ContextMenuCommandData;
|
||||
moveNotesTo: ContextMenuCommandData;
|
||||
cloneNotesTo: ContextMenuCommandData;
|
||||
deleteNotes: ContextMenuCommandData;
|
||||
importIntoNote: ContextMenuCommandData;
|
||||
exportNote: ContextMenuCommandData;
|
||||
searchInSubtree: ContextMenuCommandData;
|
||||
|
||||
addNoteLauncher: ContextMenuCommandData;
|
||||
addScriptLauncher: ContextMenuCommandData;
|
||||
addWidgetLauncher: ContextMenuCommandData;
|
||||
addSpacerLauncher: ContextMenuCommandData;
|
||||
moveLauncherToVisible: ContextMenuCommandData;
|
||||
moveLauncherToAvailable: ContextMenuCommandData;
|
||||
resetLauncher: ContextMenuCommandData;
|
||||
|
||||
executeInActiveNoteDetailWidget: CommandData & {
|
||||
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void
|
||||
};
|
||||
@ -92,17 +141,11 @@ type CommandMappings = {
|
||||
showPasswordNotSet: CommandData;
|
||||
showProtectedSessionPasswordDialog: CommandData;
|
||||
closeProtectedSessionPasswordDialog: CommandData;
|
||||
resetLauncher: CommandData;
|
||||
addNoteLauncher: CommandData;
|
||||
addScriptLauncher: CommandData;
|
||||
addWidgetLauncher: CommandData;
|
||||
addSpacerLauncher: CommandData;
|
||||
moveLauncherToVisible: CommandData;
|
||||
moveLauncherToAvailable: CommandData;
|
||||
duplicateSubtree: CommandData;
|
||||
deleteNotes: CommandData;
|
||||
copyImageReferenceToClipboard: CommandData;
|
||||
copyImageToClipboard: CommandData;
|
||||
updateAttributesList: {
|
||||
attributes: FAttribute[];
|
||||
};
|
||||
}
|
||||
|
||||
type EventMappings = {
|
||||
@ -123,9 +166,19 @@ type EventMappings = {
|
||||
|
||||
type CommandAndEventMappings = (CommandMappings & EventMappings);
|
||||
|
||||
/**
|
||||
* This type is a discriminated union which contains all the possible commands that can be triggered via {@link AppContext.triggerCommand}.
|
||||
*/
|
||||
export type CommandNames = keyof CommandMappings;
|
||||
type EventNames = keyof EventMappings;
|
||||
|
||||
type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType ? K : never; }[keyof T];
|
||||
|
||||
/**
|
||||
* Generic which filters {@link CommandNames} to provide only those commands that take in as data the desired implementation of {@link CommandData}. Mostly useful for contextual menu, to enforce consistency in the commands.
|
||||
*/
|
||||
export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>;
|
||||
|
||||
class AppContext extends Component {
|
||||
|
||||
isMainWindow: boolean;
|
||||
@ -225,9 +278,8 @@ class AppContext extends Component {
|
||||
return this.handleEvent(name, data);
|
||||
}
|
||||
|
||||
// TODO: Remove ignore once all commands are mapped out.
|
||||
//@ts-ignore
|
||||
triggerCommand<K extends CommandNames>(name: K, data: CommandMappings[K] = {}) {
|
||||
triggerCommand<K extends CommandNames>(name: K, _data?: CommandMappings[K]) {
|
||||
const data = _data || {};
|
||||
for (const executor of this.components) {
|
||||
const fun = (executor as any)[`${name}Command`];
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import utils from '../services/utils.js';
|
||||
import { CommandMappings, CommandNames } from './app_context.js';
|
||||
|
||||
/**
|
||||
* Abstract class for all components in the Trilium's frontend.
|
||||
@ -84,7 +85,8 @@ export default class Component {
|
||||
return promises.length > 0 ? Promise.all(promises) : null;
|
||||
}
|
||||
|
||||
triggerCommand(name: string, data = {}): Promise<unknown> | undefined | null {
|
||||
triggerCommand<K extends CommandNames>(name: string, _data?: CommandMappings[K]): Promise<unknown> | undefined | null {
|
||||
const data = _data || {};
|
||||
const fun = (this as any)[`${name}Command`];
|
||||
|
||||
if (fun) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { MenuCommandItem } from "../menus/context_menu.js";
|
||||
import { CommandNames } from "./app_context.js";
|
||||
|
||||
type ListenerReturnType = void | Promise<void>;
|
||||
|
||||
export interface SelectMenuItemEventListener {
|
||||
selectMenuItemHandler(item: MenuCommandItem): ListenerReturnType;
|
||||
export interface SelectMenuItemEventListener<T extends CommandNames> {
|
||||
selectMenuItemHandler(item: MenuCommandItem<T>): ListenerReturnType;
|
||||
}
|
||||
|
@ -1,39 +1,39 @@
|
||||
import { CommandNames } from '../components/app_context.js';
|
||||
import keyboardActionService from '../services/keyboard_actions.js';
|
||||
|
||||
interface ContextMenuOptions {
|
||||
interface ContextMenuOptions<T extends CommandNames> {
|
||||
x: number;
|
||||
y: number;
|
||||
orientation?: "left";
|
||||
selectMenuItemHandler: MenuHandler;
|
||||
items: MenuItem[];
|
||||
selectMenuItemHandler: MenuHandler<T>;
|
||||
items: MenuItem<T>[];
|
||||
}
|
||||
|
||||
interface MenuSeparatorItem {
|
||||
title: "----"
|
||||
}
|
||||
|
||||
export interface MenuCommandItem {
|
||||
export interface MenuCommandItem<T extends CommandNames> {
|
||||
title: string;
|
||||
command?: CommandNames;
|
||||
command?: T;
|
||||
type?: string;
|
||||
uiIcon?: string;
|
||||
templateNoteId?: string;
|
||||
enabled?: boolean;
|
||||
handler?: MenuHandler;
|
||||
items?: MenuItem[];
|
||||
handler?: MenuHandler<T>;
|
||||
items?: MenuItem<T>[] | null;
|
||||
shortcut?: string;
|
||||
spellingSuggestion?: string;
|
||||
}
|
||||
|
||||
export type MenuItem = MenuCommandItem | MenuSeparatorItem;
|
||||
export type MenuHandler = (item: MenuCommandItem, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
|
||||
class ContextMenu {
|
||||
|
||||
private $widget!: JQuery<HTMLElement>;
|
||||
private dateContextMenuOpenedMs: number;
|
||||
private options?: ContextMenuOptions;
|
||||
private options?: ContextMenuOptions<any>;
|
||||
|
||||
constructor() {
|
||||
this.$widget = $("#context-menu-container");
|
||||
@ -43,7 +43,7 @@ class ContextMenu {
|
||||
$(document).on('click', () => this.hide());
|
||||
}
|
||||
|
||||
async show(options: ContextMenuOptions) {
|
||||
async show<T extends CommandNames>(options: ContextMenuOptions<T>) {
|
||||
this.options = options;
|
||||
|
||||
if (this.$widget.hasClass("show")) {
|
||||
@ -119,7 +119,7 @@ class ContextMenu {
|
||||
}).addClass("show");
|
||||
}
|
||||
|
||||
addItems($parent: JQuery<HTMLElement>, items: MenuItem[]) {
|
||||
addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[]) {
|
||||
for (const item of items) {
|
||||
if (!item) {
|
||||
continue;
|
||||
|
@ -4,6 +4,7 @@ import zoomService from "../components/zoom.js";
|
||||
import contextMenu, { MenuItem } from "./context_menu.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type { BrowserWindow } from "electron";
|
||||
import { CommandNames } from "../components/app_context.js";
|
||||
|
||||
function setupContextMenu() {
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
@ -18,7 +19,7 @@ function setupContextMenu() {
|
||||
const isMac = process.platform === "darwin";
|
||||
const platformModifier = isMac ? 'Meta' : 'Ctrl';
|
||||
|
||||
const items: MenuItem[] = [];
|
||||
const items: MenuItem<CommandNames>[] = [];
|
||||
|
||||
if (params.misspelledWord) {
|
||||
for (const suggestion of params.dictionarySuggestions) {
|
||||
|
@ -6,8 +6,11 @@ import server from "../services/server.js";
|
||||
import { t } from '../services/i18n.js';
|
||||
import type { SelectMenuItemEventListener } from '../components/events.js';
|
||||
import NoteTreeWidget from '../widgets/note_tree.js';
|
||||
import { FilteredCommandNames, ContextMenuCommandData } from '../components/app_context.js';
|
||||
|
||||
export default class LauncherContextMenu implements SelectMenuItemEventListener {
|
||||
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
|
||||
|
||||
export default class LauncherContextMenu implements SelectMenuItemEventListener<LauncherCommandNames> {
|
||||
|
||||
private treeWidget: NoteTreeWidget;
|
||||
private node: Node;
|
||||
@ -26,7 +29,7 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener
|
||||
})
|
||||
}
|
||||
|
||||
async getMenuItems(): Promise<MenuItem[]> {
|
||||
async getMenuItems(): Promise<MenuItem<LauncherCommandNames>[]> {
|
||||
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
|
||||
const parentNoteId = this.node.getParent().data.noteId;
|
||||
|
||||
@ -38,7 +41,7 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener
|
||||
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
|
||||
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
|
||||
|
||||
const items: (MenuItem | null)[] = [
|
||||
const items: (MenuItem<LauncherCommandNames> | null)[] = [
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-note" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-code-curly" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-customize" } : null,
|
||||
@ -59,7 +62,7 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener
|
||||
return items.filter(row => row !== null);
|
||||
}
|
||||
|
||||
async selectMenuItemHandler({command}: MenuCommandItem) {
|
||||
async selectMenuItemHandler({command}: MenuCommandItem<LauncherCommandNames>) {
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import froca from "../services/froca.js";
|
||||
import clipboard from '../services/clipboard.js';
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import contextMenu, { MenuCommandItem, MenuItem } from "./context_menu.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import appContext, { ContextMenuCommandData, FilteredCommandNames } from "../components/app_context.js";
|
||||
import noteTypesService from "../services/note_types.js";
|
||||
import server from "../services/server.js";
|
||||
import toastService from "../services/toast.js";
|
||||
@ -18,7 +18,9 @@ interface ConvertToAttachmentResponse {
|
||||
attachment?: FAttachment;
|
||||
}
|
||||
|
||||
export default class TreeContextMenu implements SelectMenuItemEventListener {
|
||||
type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData>;
|
||||
|
||||
export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> {
|
||||
|
||||
private treeWidget: NoteTreeWidget;
|
||||
private node: Node;
|
||||
@ -37,7 +39,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener {
|
||||
})
|
||||
}
|
||||
|
||||
async getMenuItems(): Promise<MenuItem[]> {
|
||||
async getMenuItems(): Promise<MenuItem<TreeCommandNames>[]> {
|
||||
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
|
||||
const branch = froca.getBranch(this.node.data.branchId);
|
||||
const isNotRoot = note?.noteId !== 'root';
|
||||
@ -56,7 +58,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener {
|
||||
const parentNotSearch = !parentNote || parentNote.type !== 'search';
|
||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||
|
||||
return [
|
||||
const items: (MenuItem<TreeCommandNames> | null)[] = [
|
||||
{ title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
|
||||
|
||||
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
@ -149,10 +151,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener {
|
||||
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
|
||||
].filter(row => row !== null) as MenuItem[];
|
||||
];
|
||||
return items.filter(row => row !== null);
|
||||
}
|
||||
|
||||
async selectMenuItemHandler({command, type, templateNoteId}: MenuCommandItem) {
|
||||
async selectMenuItemHandler({command, type, templateNoteId}: MenuCommandItem<TreeCommandNames>) {
|
||||
const notePath = treeService.getNotePath(this.node);
|
||||
|
||||
if (command === 'openInTab') {
|
||||
@ -210,7 +213,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener {
|
||||
navigator.clipboard.writeText('#' + notePath);
|
||||
}
|
||||
else if (command) {
|
||||
this.treeWidget.triggerCommand(command, {
|
||||
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
|
||||
node: this.node,
|
||||
notePath: notePath,
|
||||
noteId: this.node.data.noteId,
|
||||
|
@ -2,10 +2,12 @@ import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { MenuItem } from "../menus/context_menu.js";
|
||||
import { CommandNames } from "../components/app_context.js";
|
||||
import { ContextMenuCommandData, FilteredCommandNames } from "../components/app_context.js";
|
||||
|
||||
async function getNoteTypeItems(command?: CommandNames) {
|
||||
const items: MenuItem[] = [
|
||||
type NoteTypeCommandNames = FilteredCommandNames<ContextMenuCommandData>;
|
||||
|
||||
async function getNoteTypeItems(command?: NoteTypeCommandNames) {
|
||||
const items: MenuItem<NoteTypeCommandNames>[] = [
|
||||
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" },
|
||||
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" },
|
||||
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { CommandNames } from "../../components/app_context.js";
|
||||
import { MenuCommandItem } from "../../menus/context_menu.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import noteTypesService from "../../services/note_types.js";
|
||||
@ -128,7 +129,7 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
if (noteType.title === '----') {
|
||||
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
|
||||
} else {
|
||||
const commandItem = (noteType as MenuCommandItem)
|
||||
const commandItem = (noteType as MenuCommandItem<CommandNames>)
|
||||
this.$noteTypeDropdown.append(
|
||||
$('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", commandItem.type || "")
|
||||
|
Loading…
x
Reference in New Issue
Block a user