Merge pull request #1268 from TriliumNext/port/client_ts

Port note tree to TypeScript
This commit is contained in:
Elian Doran 2025-02-24 21:59:31 +02:00 committed by GitHub
commit d85c670d7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1458 additions and 205 deletions

View File

@ -18,7 +18,6 @@ import type NoteDetailWidget from "../widgets/note_detail.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js"; import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
import type { Node } from "../services/tree.js";
import type LoadResults from "../services/load_results.js"; import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js"; import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js"; import type NoteTreeWidget from "../widgets/note_tree.js";
@ -49,15 +48,15 @@ export interface CommandData {
* Represents a set of commands that are triggered from the context menu, providing information such as the selected note. * Represents a set of commands that are triggered from the context menu, providing information such as the selected note.
*/ */
export interface ContextMenuCommandData extends CommandData { export interface ContextMenuCommandData extends CommandData {
node: Node; node: Fancytree.FancytreeNode;
notePath: string; notePath?: string;
noteId?: string; noteId?: string;
selectedOrActiveBranchIds: any; // TODO: Remove any once type is defined selectedOrActiveBranchIds?: any; // TODO: Remove any once type is defined
selectedOrActiveNoteIds: any; // TODO: Remove any once type is defined selectedOrActiveNoteIds: any; // TODO: Remove any once type is defined
} }
export interface NoteCommandData extends CommandData { export interface NoteCommandData extends CommandData {
notePath: string; notePath?: string;
hoistedNoteId?: string; hoistedNoteId?: string;
viewScope?: ViewScope; viewScope?: ViewScope;
} }
@ -72,6 +71,7 @@ export interface ExecuteCommandData<T> extends CommandData {
export type CommandMappings = { export type CommandMappings = {
"api-log-messages": CommandData; "api-log-messages": CommandData;
focusTree: CommandData, focusTree: CommandData,
focusOnTitle: CommandData;
focusOnDetail: CommandData; focusOnDetail: CommandData;
focusOnSearchDefinition: Required<CommandData>; focusOnSearchDefinition: Required<CommandData>;
searchNotes: CommandData & { searchNotes: CommandData & {
@ -79,6 +79,7 @@ export type CommandMappings = {
ancestorNoteId?: string | null; ancestorNoteId?: string | null;
}; };
closeTocCommand: CommandData; closeTocCommand: CommandData;
closeHlt: CommandData;
showLaunchBarSubtree: CommandData; showLaunchBarSubtree: CommandData;
showRevisions: CommandData; showRevisions: CommandData;
showOptions: CommandData & { showOptions: CommandData & {
@ -106,13 +107,18 @@ export type CommandMappings = {
showPromptDialog: PromptDialogOptions; showPromptDialog: PromptDialogOptions;
showInfoDialog: ConfirmWithMessageOptions; showInfoDialog: ConfirmWithMessageOptions;
showConfirmDialog: ConfirmWithMessageOptions; showConfirmDialog: ConfirmWithMessageOptions;
showRecentChanges: CommandData & { ancestorNoteId: string };
showImportDialog: CommandData & { noteId: string; };
openNewNoteSplit: NoteCommandData; openNewNoteSplit: NoteCommandData;
openInWindow: NoteCommandData; openInWindow: NoteCommandData;
openNoteInNewTab: CommandData; openNoteInNewTab: CommandData;
openNoteInNewSplit: CommandData; openNoteInNewSplit: CommandData;
openNoteInNewWindow: CommandData; openNoteInNewWindow: CommandData;
openAboutDialog: CommandData;
hideFloatingButtons: {};
hideLeftPane: CommandData; hideLeftPane: CommandData;
showLeftPane: CommandData; showLeftPane: CommandData;
hoistNote: CommandData & { noteId: string };
leaveProtectedSession: CommandData; leaveProtectedSession: CommandData;
enterProtectedSession: CommandData; enterProtectedSession: CommandData;
@ -122,9 +128,12 @@ export type CommandMappings = {
insertNoteAfter: ContextMenuCommandData; insertNoteAfter: ContextMenuCommandData;
insertChildNote: ContextMenuCommandData; insertChildNote: ContextMenuCommandData;
delete: ContextMenuCommandData; delete: ContextMenuCommandData;
editNoteTitle: ContextMenuCommandData;
protectSubtree: ContextMenuCommandData; protectSubtree: ContextMenuCommandData;
unprotectSubtree: ContextMenuCommandData; unprotectSubtree: ContextMenuCommandData;
openBulkActionsDialog: ContextMenuCommandData; openBulkActionsDialog: ContextMenuCommandData | {
selectedOrActiveNoteIds?: string[]
};
editBranchPrefix: ContextMenuCommandData; editBranchPrefix: ContextMenuCommandData;
convertNoteToAttachment: ContextMenuCommandData; convertNoteToAttachment: ContextMenuCommandData;
duplicateSubtree: ContextMenuCommandData; duplicateSubtree: ContextMenuCommandData;
@ -143,6 +152,11 @@ export type CommandMappings = {
importIntoNote: ContextMenuCommandData; importIntoNote: ContextMenuCommandData;
exportNote: ContextMenuCommandData; exportNote: ContextMenuCommandData;
searchInSubtree: ContextMenuCommandData; searchInSubtree: ContextMenuCommandData;
moveNoteUp: ContextMenuCommandData;
moveNoteDown: ContextMenuCommandData;
moveNoteUpInHierarchy: ContextMenuCommandData;
moveNoteDownInHierarchy: ContextMenuCommandData;
selectAllNotesInParent: ContextMenuCommandData;
addNoteLauncher: ContextMenuCommandData; addNoteLauncher: ContextMenuCommandData;
addScriptLauncher: ContextMenuCommandData; addScriptLauncher: ContextMenuCommandData;
@ -175,6 +189,7 @@ export type CommandMappings = {
importMarkdownInline: CommandData; importMarkdownInline: CommandData;
showPasswordNotSet: CommandData; showPasswordNotSet: CommandData;
showProtectedSessionPasswordDialog: CommandData; showProtectedSessionPasswordDialog: CommandData;
showUploadAttachmentsDialog: CommandData & { noteId: string };
closeProtectedSessionPasswordDialog: CommandData; closeProtectedSessionPasswordDialog: CommandData;
copyImageReferenceToClipboard: CommandData; copyImageReferenceToClipboard: CommandData;
copyImageToClipboard: CommandData; copyImageToClipboard: CommandData;
@ -198,6 +213,7 @@ export type CommandMappings = {
screen: Screen; screen: Screen;
}; };
closeTab: CommandData; closeTab: CommandData;
closeToc: CommandData;
closeOtherTabs: CommandData; closeOtherTabs: CommandData;
closeRightTabs: CommandData; closeRightTabs: CommandData;
closeAllTabs: CommandData; closeAllTabs: CommandData;
@ -216,15 +232,20 @@ export type CommandMappings = {
scrollContainerToCommand: CommandData & { scrollContainerToCommand: CommandData & {
position: number; position: number;
}; };
moveThisNoteSplit: CommandData & { scrollToEnd: CommandData;
isMovingLeft: boolean; closeThisNoteSplit: CommandData;
}; moveThisNoteSplit: CommandData & { isMovingLeft: boolean; };
// Geomap // Geomap
deleteFromMap: { noteId: string }, deleteFromMap: { noteId: string },
openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent } openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent }
toggleZenMode: CommandData; toggleZenMode: CommandData;
updateAttributeList: CommandData & { attributes: Attribute[] };
saveAttributes: CommandData;
reloadAttributes: CommandData;
refreshNoteList: CommandData & { noteId: string; };
}; };
type EventMappings = { type EventMappings = {
@ -329,7 +350,6 @@ type EventMappings = {
showToc: { showToc: {
noteId: string; noteId: string;
}; };
scrollToEnd: { ntxId: string };
noteTypeMimeChanged: { noteId: string }; noteTypeMimeChanged: { noteId: string };
zenModeChanged: { isEnabled: boolean }; zenModeChanged: { isEnabled: boolean };
}; };

View File

@ -80,8 +80,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return promises.length > 0 ? Promise.all(promises) : null; return promises.length > 0 ? Promise.all(promises) : null;
} }
triggerCommand<K extends CommandNames>(name: string, _data?: CommandMappings[K]): Promise<unknown> | undefined | null { triggerCommand<K extends CommandNames>(name: K, data?: CommandMappings[K]): Promise<unknown> | undefined | null {
const data = _data || {};
const fun = (this as any)[`${name}Command`]; const fun = (this as any)[`${name}Command`];
if (fun) { if (fun) {

View File

@ -11,7 +11,7 @@ import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js";
interface SetNoteOpts { export interface SetNoteOpts {
triggerSwitchEvent?: unknown; triggerSwitchEvent?: unknown;
viewScope?: ViewScope; viewScope?: ViewScope;
} }

View File

@ -8,6 +8,7 @@ export interface FBranchRow {
prefix?: string; prefix?: string;
isExpanded?: boolean; isExpanded?: boolean;
fromSearchNote: boolean; fromSearchNote: boolean;
isDeleted?: boolean;
} }
/** /**

View File

@ -1,4 +1,4 @@
import treeService, { type Node } from "../services/tree.js"; import treeService from "../services/tree.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js"; import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import dialogService from "../services/dialog.js"; import dialogService from "../services/dialog.js";
@ -12,17 +12,17 @@ type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
export default class LauncherContextMenu implements SelectMenuItemEventListener<LauncherCommandNames> { export default class LauncherContextMenu implements SelectMenuItemEventListener<LauncherCommandNames> {
private treeWidget: NoteTreeWidget; private treeWidget: NoteTreeWidget;
private node: Node; private node: Fancytree.FancytreeNode;
constructor(treeWidget: NoteTreeWidget, node: Node) { constructor(treeWidget: NoteTreeWidget, node: Fancytree.FancytreeNode) {
this.treeWidget = treeWidget; this.treeWidget = treeWidget;
this.node = node; this.node = node;
} }
async show(e: PointerEvent) { async show(e: PointerEvent | JQuery.TouchStartEvent | JQuery.ContextMenuEvent) {
contextMenu.show({ contextMenu.show({
x: e.pageX, x: e.pageX ?? 0,
y: e.pageY, y: e.pageY ?? 0,
items: await this.getMenuItems(), items: await this.getMenuItems(),
selectMenuItemHandler: (item, e) => this.selectMenuItemHandler(item) selectMenuItemHandler: (item, e) => this.selectMenuItemHandler(item)
}); });

View File

@ -1,4 +1,4 @@
import treeService, { type Node } from "../services/tree.js"; import treeService from "../services/tree.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import clipboard from "../services/clipboard.js"; import clipboard from "../services/clipboard.js";
import noteCreateService from "../services/note_create.js"; import noteCreateService from "../services/note_create.js";
@ -18,21 +18,23 @@ interface ConvertToAttachmentResponse {
attachment?: FAttachment; attachment?: FAttachment;
} }
type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData>; // This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator,
// so they need to be added manually.
export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog";
export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> { export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> {
private treeWidget: NoteTreeWidget; private treeWidget: NoteTreeWidget;
private node: Node; private node: Fancytree.FancytreeNode;
constructor(treeWidget: NoteTreeWidget, node: Node) { constructor(treeWidget: NoteTreeWidget, node: Fancytree.FancytreeNode) {
this.treeWidget = treeWidget; this.treeWidget = treeWidget;
this.node = node; this.node = node;
} }
async show(e: PointerEvent) { async show(e: PointerEvent | JQuery.TouchStartEvent | JQuery.ContextMenuEvent) {
contextMenu.show({ contextMenu.show({
x: e.pageX, x: e.pageX ?? 0,
y: e.pageY, y: e.pageY ?? 0,
items: await this.getMenuItems(), items: await this.getMenuItems(),
selectMenuItemHandler: (item, e) => this.selectMenuItemHandler(item) selectMenuItemHandler: (item, e) => this.selectMenuItemHandler(item)
}); });

View File

@ -6,7 +6,6 @@ import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js"; import ws from "./ws.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import type { Node } from "./tree.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
// TODO: Deduplicate type with server // TODO: Deduplicate type with server
@ -160,7 +159,7 @@ async function activateParentNotePath() {
} }
} }
async function moveNodeUpInHierarchy(node: Node) { async function moveNodeUpInHierarchy(node: Fancytree.FancytreeNode) {
if (hoistedNoteService.isHoistedNode(node) || hoistedNoteService.isTopLevelNode(node) || node.getParent().data.noteType === "search") { if (hoistedNoteService.isHoistedNode(node) || hoistedNoteService.isTopLevelNode(node) || node.getParent().data.noteType === "search") {
return; return;
} }

View File

@ -1,5 +1,5 @@
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import treeService, { type Node } from "./tree.js"; import treeService from "./tree.js";
import dialogService from "./dialog.js"; import dialogService from "./dialog.js";
import froca from "./froca.js"; import froca from "./froca.js";
import type NoteContext from "../components/note_context.js"; import type NoteContext from "../components/note_context.js";
@ -19,11 +19,11 @@ async function unhoist() {
} }
} }
function isTopLevelNode(node: Node) { function isTopLevelNode(node: Fancytree.FancytreeNode) {
return isHoistedNode(node.getParent()); return isHoistedNode(node.getParent());
} }
function isHoistedNode(node: Node) { function isHoistedNode(node: Fancytree.FancytreeNode) {
// even though check for 'root' should not be necessary, we keep it just in case // even though check for 'root' should not be necessary, we keep it just in case
return node.data.noteId === "root" || node.data.noteId === getHoistedNoteId(); return node.data.noteId === "root" || node.data.noteId === getHoistedNoteId();
} }

View File

@ -5,7 +5,16 @@ import utils from "./utils.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[], options: Record<string, string | Blob>) { interface UploadFilesOptions {
safeImport: boolean;
shrinkImages: boolean;
textImportedAsText: boolean;
codeImportedAsCode: boolean;
explodeArchives: boolean;
replaceUnderscoresWithSpaces: boolean;
}
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[], options: UploadFilesOptions) {
if (!["notes", "attachments"].includes(entityType)) { if (!["notes", "attachments"].includes(entityType)) {
throw new Error(`Unrecognized import entity type '${entityType}'.`); throw new Error(`Unrecognized import entity type '${entityType}'.`);
} }
@ -26,7 +35,7 @@ export async function uploadFiles(entityType: string, parentNoteId: string, file
formData.append("last", counter === files.length ? "true" : "false"); formData.append("last", counter === files.length ? "true" : "false");
for (const key in options) { for (const key in options) {
formData.append(key, options[key]); formData.append(key, (options as any)[key]);
} }
await $.ajax({ await $.ajax({

View File

@ -8,11 +8,14 @@ interface NoteRow {
isDeleted?: boolean; isDeleted?: boolean;
} }
interface BranchRow { // TODO: Deduplicate with BranchRow from `rows.ts`/
export interface BranchRow {
noteId?: string; noteId?: string;
branchId: string; branchId: string;
componentId: string; componentId: string;
parentNoteId?: string; parentNoteId?: string;
isDeleted?: boolean;
isExpanded?: boolean;
} }
export interface AttributeRow { export interface AttributeRow {

View File

@ -2,12 +2,10 @@ import server from "./server.js";
import froca from "./froca.js"; import froca from "./froca.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import type { MenuItem } from "../menus/context_menu.js"; import type { MenuItem } from "../menus/context_menu.js";
import type { ContextMenuCommandData, FilteredCommandNames } from "../components/app_context.js"; import type { TreeCommandNames } from "../menus/tree_context_menu.js";
type NoteTypeCommandNames = FilteredCommandNames<ContextMenuCommandData>; async function getNoteTypeItems(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [
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.text"), command, type: "text", uiIcon: "bx bx-note" },
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" }, { 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" }, { title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" },

View File

@ -4,20 +4,6 @@ import froca from "./froca.js";
import hoistedNoteService from "../services/hoisted_note.js"; import hoistedNoteService from "../services/hoisted_note.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
export interface Node {
title: string;
getParent(): Node;
getChildren(): Node[];
folder: boolean;
renderTitle(): void;
data: {
noteId?: string;
isProtected?: boolean;
branchId: string;
noteType: string;
};
}
/** /**
* @returns {string|null} * @returns {string|null}
*/ */
@ -148,7 +134,7 @@ ws.subscribeToMessages((message) => {
} }
}); });
function getParentProtectedStatus(node: Node) { function getParentProtectedStatus(node: Fancytree.FancytreeNode) {
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected; return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
} }
@ -205,7 +191,7 @@ function getNoteIdAndParentIdFromUrl(urlOrNotePath: string) {
}; };
} }
function getNotePath(node: Node) { function getNotePath(node: Fancytree.FancytreeNode) {
if (!node) { if (!node) {
logError("Node is null"); logError("Node is null");
return ""; return "";

View File

@ -137,7 +137,7 @@ function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
} }
function assertArguments(...args: string[]) { function assertArguments<T>(...args: T[]) {
for (const i in args) { for (const i in args) {
if (!args[i]) { if (!args[i]) {
console.trace(`Argument idx#${i} should not be falsy: ${args[i]}`); console.trace(`Argument idx#${i} should not be falsy: ${args[i]}`);

1165
src/public/app/types-fancytree.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -107,7 +107,7 @@ declare global {
} }
} }
var logError: (message: string, e?: Error) => void; var logError: (message: string, e?: Error | string) => void;
var logInfo: (message: string) => void; var logInfo: (message: string) => void;
var glob: CustomGlobals; var glob: CustomGlobals;
var require: RequireMethod; var require: RequireMethod;

View File

@ -125,7 +125,7 @@ class NoteContextAwareWidget extends BasicWidget {
} }
} }
async frocaReloadedEvent() { async frocaReloadedEvent(): Promise<void> {
await this.refresh(); await this.refresh();
} }
} }

View File

@ -9,7 +9,7 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js"; import server from "../services/server.js";
import noteCreateService from "../services/note_create.js"; import noteCreateService from "../services/note_create.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import appContext from "../components/app_context.js"; import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import keyboardActionsService from "../services/keyboard_actions.js"; import keyboardActionsService from "../services/keyboard_actions.js";
import clipboard from "../services/clipboard.js"; import clipboard from "../services/clipboard.js";
import protectedSessionService from "../services/protected_session.js"; import protectedSessionService from "../services/protected_session.js";
@ -19,6 +19,12 @@ import protectedSessionHolder from "../services/protected_session_holder.js";
import dialogService from "../services/dialog.js"; import dialogService from "../services/dialog.js";
import shortcutService from "../services/shortcuts.js"; import shortcutService from "../services/shortcuts.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import type FBranch from "../entities/fbranch.js";
import type LoadResults from "../services/load_results.js";
import type FNote from "../entities/fnote.js";
import type { NoteType } from "../entities/fnote.js";
import type { AttributeRow, BranchRow } from "../services/load_results.js";
import type { SetNoteOpts } from "../components/note_context.js";
const TPL = ` const TPL = `
<div class="tree-wrapper"> <div class="tree-wrapper">
@ -139,9 +145,54 @@ const TPL = `
const MAX_SEARCH_RESULTS_IN_TREE = 100; const MAX_SEARCH_RESULTS_IN_TREE = 100;
// this has to be hanged on the actual elements to effectively intercept and stop click event // this has to be hanged on the actual elements to effectively intercept and stop click event
const cancelClickPropagation = (e) => e.stopPropagation(); const cancelClickPropagation: JQuery.TypeEventHandler<unknown, unknown, unknown, unknown, any> = (e) => e.stopPropagation();
// TODO: Fix once we remove Node.js API from public
type Timeout = NodeJS.Timeout | string | number | undefined;
// TODO: Deduplicate with server special_notes
type LauncherType = "launcher" | "note" | "script" | "customWidget" | "spacer";
// TODO: Deduplicate with the server
interface CreateLauncherResponse {
success: boolean;
message: string;
note: {
noteId: string;
}
}
interface ExpandedSubtreeResponse {
branchIds: string[]
}
interface Node extends Fancytree.NodeData {
noteId: string;
parentNoteId: string;
branchId: string;
isProtected: boolean;
noteType: NoteType;
}
interface RefreshContext {
noteIdsToUpdate: Set<string>;
noteIdsToReload: Set<string>;
}
export default class NoteTreeWidget extends NoteContextAwareWidget { export default class NoteTreeWidget extends NoteContextAwareWidget {
private $tree!: JQuery<HTMLElement>;
private $treeActions!: JQuery<HTMLElement>;
private $treeSettingsButton!: JQuery<HTMLElement>;
private $treeSettingsPopup!: JQuery<HTMLElement>;
private $saveTreeSettingsButton!: JQuery<HTMLElement>;
private $hideArchivedNotesCheckbox!: JQuery<HTMLElement>;
private $autoCollapseNoteTree!: JQuery<HTMLElement>;
private treeName: "main";
private autoCollapseTimeoutId?: Timeout;
private lastFilteredHoistedNotePath?: string | null;
private tree!: Fancytree.Fancytree;
constructor() { constructor() {
super(); super();
@ -156,7 +207,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist()); this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist());
this.$tree.on("mousedown", ".refresh-search-button", (e) => this.refreshSearch(e)); this.$tree.on("mousedown", ".refresh-search-button", (e) => this.refreshSearch(e));
this.$tree.on("mousedown", ".add-note-button", (e) => { this.$tree.on("mousedown", ".add-note-button", (e) => {
const node = $.ui.fancytree.getNode(e); const node = $.ui.fancytree.getNode(e as unknown as Event);
const parentNotePath = treeService.getNotePath(node); const parentNotePath = treeService.getNotePath(node);
noteCreateService.createNote(parentNotePath, { noteCreateService.createNote(parentNotePath, {
@ -165,7 +216,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}); });
this.$tree.on("mousedown", ".enter-workspace-button", (e) => { this.$tree.on("mousedown", ".enter-workspace-button", (e) => {
const node = $.ui.fancytree.getNode(e); const node = $.ui.fancytree.getNode(e as unknown as Event);
this.triggerCommand("hoistNote", { noteId: node.data.noteId }); this.triggerCommand("hoistNote", { noteId: node.data.noteId });
}); });
@ -173,7 +224,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// fancytree doesn't support middle click, so this is a way to support it // fancytree doesn't support middle click, so this is a way to support it
this.$tree.on("mousedown", ".fancytree-title", (e) => { this.$tree.on("mousedown", ".fancytree-title", (e) => {
if (e.which === 2) { if (e.which === 2) {
const node = $.ui.fancytree.getNode(e); const node = $.ui.fancytree.getNode(e as unknown as Event);
const notePath = treeService.getNotePath(node); const notePath = treeService.getNotePath(node);
@ -200,8 +251,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes); this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes);
this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree); this.$autoCollapseNoteTree.prop("checked", this.autoCollapseNoteTree);
const top = this.$treeActions[0].offsetTop - this.$treeSettingsPopup.outerHeight(); const top = this.$treeActions[0].offsetTop - (this.$treeSettingsPopup.outerHeight() ?? 0);
const left = Math.max(0, this.$treeActions[0].offsetLeft - this.$treeSettingsPopup.outerWidth() + this.$treeActions.outerWidth()); const left = Math.max(0, this.$treeActions[0].offsetLeft - (this.$treeSettingsPopup.outerWidth() ?? 0) + (this.$treeActions.outerWidth() ?? 0));
this.$treeSettingsPopup this.$treeSettingsPopup
.css({ .css({
@ -241,16 +292,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// see https://github.com/zadam/trilium/pull/1120 for discussion // see https://github.com/zadam/trilium/pull/1120 for discussion
// code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d // code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d
const isEnclosing = ($container, $sub) => { const isEnclosing = ($container: JQuery<HTMLElement>, $sub: JQuery<HTMLElement>) => {
const conOffset = $container.offset(); const conOffset = $container.offset();
const conDistanceFromTop = conOffset.top + $container.outerHeight(true); const conDistanceFromTop = (conOffset?.top ?? 0) + ($container.outerHeight(true) ?? 0);
const conDistanceFromLeft = conOffset.left + $container.outerWidth(true); const conDistanceFromLeft = (conOffset?.left ?? 0) + ($container.outerWidth(true) ?? 0);
const subOffset = $sub.offset(); const subOffset = $sub.offset();
const subDistanceFromTop = subOffset.top + $sub.outerHeight(true); const subDistanceFromTop = (subOffset?.top ?? 0) + ($sub.outerHeight(true) ?? 0);
const subDistanceFromLeft = subOffset.left + $sub.outerWidth(true); const subDistanceFromLeft = (subOffset?.left ?? 0) + ($sub.outerWidth(true) ?? 0);
return conDistanceFromTop > subDistanceFromTop && conOffset.top < subOffset.top && conDistanceFromLeft > subDistanceFromLeft && conOffset.left < subOffset.left; return conDistanceFromTop > subDistanceFromTop
&& (conOffset?.top ?? 0) < (subOffset?.top ?? 0)
&& conDistanceFromLeft > subDistanceFromLeft
&& (conOffset?.left ?? 0) < (subOffset?.left ?? 0);
}; };
this.$tree.on("mouseenter", "span.fancytree-title", (e) => { this.$tree.on("mouseenter", "span.fancytree-title", (e) => {
@ -262,7 +316,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return options.is(`hideArchivedNotes_${this.treeName}`); return options.is(`hideArchivedNotes_${this.treeName}`);
} }
async setHideArchivedNotes(val) { async setHideArchivedNotes(val: string) {
await options.save(`hideArchivedNotes_${this.treeName}`, val.toString()); await options.save(`hideArchivedNotes_${this.treeName}`, val.toString());
} }
@ -270,7 +324,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return options.is("autoCollapseNoteTree"); return options.is("autoCollapseNoteTree");
} }
async setAutoCollapseNoteTree(val) { async setAutoCollapseNoteTree(val: string) {
await options.save("autoCollapseNoteTree", val.toString()); await options.save("autoCollapseNoteTree", val.toString());
} }
@ -288,7 +342,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}, },
scrollParent: this.$tree, scrollParent: this.$tree,
minExpandLevel: 2, // root can't be collapsed minExpandLevel: 2, // root can't be collapsed
click: (event, data) => { click: (event, data): boolean => {
this.activityDetected(); this.activityDetected();
const targetType = data.targetType; const targetType = data.targetType;
@ -305,12 +359,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const activeNode = this.getActiveNode(); const activeNode = this.getActiveNode();
if (activeNode.getParent() !== node.getParent()) { if (activeNode.getParent() !== node.getParent()) {
return; return true;
} }
this.clearSelectedNodes(); this.clearSelectedNodes();
function selectInBetween(first, second) { function selectInBetween(first: Fancytree.FancytreeNode, second: Fancytree.FancytreeNode) {
for (let i = 0; first && first !== second && i < 10000; i++) { for (let i = 0; first && first !== second && i < 10000; i++) {
first.setSelected(true); first.setSelected(true);
first = first.getNextSibling(); first = first.getNextSibling();
@ -334,13 +388,15 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
node.setFocus(true); node.setFocus(true);
} else if (data.node.isActive()) { } else if (data.node.isActive()) {
// this is important for single column mobile view, otherwise it's not possible to see again previously displayed note // this is important for single column mobile view, otherwise it's not possible to see again previously displayed note
this.tree.reactivate(true); this.tree.reactivate();
} else { } else {
node.setActive(); node.setActive();
} }
return false; return false;
} }
return true;
}, },
beforeActivate: (event, { node }) => { beforeActivate: (event, { node }) => {
// hidden subtree is hidden hackily - we want it to be present in the tree so that we can switch to it // hidden subtree is hidden hackily - we want it to be present in the tree so that we can switch to it
@ -368,8 +424,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const notePath = treeService.getNotePath(data.node); const notePath = treeService.getNotePath(data.node);
const activeNoteContext = appContext.tabManager.getActiveContext(); const activeNoteContext = appContext.tabManager.getActiveContext();
const opts = {}; const opts: SetNoteOpts = {};
if (activeNoteContext.viewScope.viewMode === "contextual-help") { if (activeNoteContext.viewScope?.viewMode === "contextual-help") {
opts.viewScope = activeNoteContext.viewScope; opts.viewScope = activeNoteContext.viewScope;
} }
await activeNoteContext.setNote(notePath, opts); await activeNoteContext.setNote(notePath, opts);
@ -450,7 +506,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}); });
} else { } else {
const jsonStr = dataTransfer.getData("text"); const jsonStr = dataTransfer.getData("text");
let notes = null; let notes: BranchRow[];
try { try {
notes = JSON.parse(jsonStr); notes = JSON.parse(jsonStr);
@ -462,7 +518,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// This function MUST be defined to enable dropping of items on the tree. // This function MUST be defined to enable dropping of items on the tree.
// data.hitMode is 'before', 'after', or 'over'. // data.hitMode is 'before', 'after', or 'over'.
const selectedBranchIds = notes.map((note) => note.branchId); const selectedBranchIds = notes
.map((note) => note.branchId)
.filter((branchId) => branchId) as string[];
if (data.hitMode === "before") { if (data.hitMode === "before") {
branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId); branchService.moveBeforeBranch(selectedBranchIds, node.data.branchId);
@ -513,7 +571,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
clones: { clones: {
highlightActiveClones: true highlightActiveClones: true
}, },
enhanceTitle: async function (event, data) { enhanceTitle: async function (event: Event, data: {
node: Fancytree.FancytreeNode;
noteId: string;
}) {
const node = data.node; const node = data.node;
if (!node.data.noteId) { if (!node.data.noteId) {
@ -602,7 +663,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const isMobile = utils.isMobile(); const isMobile = utils.isMobile();
if (isMobile) { if (isMobile) {
let showTimeout; let showTimeout: Timeout;
this.$tree.on("touchstart", ".fancytree-node", (e) => { this.$tree.on("touchstart", ".fancytree-node", (e) => {
touchStart = new Date().getTime(); touchStart = new Date().getTime();
@ -642,8 +703,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.tree = $.ui.fancytree.getTree(this.$tree); this.tree = $.ui.fancytree.getTree(this.$tree);
} }
showContextMenu(e) { showContextMenu(e: PointerEvent | JQuery.TouchStartEvent | JQuery.ContextMenuEvent) {
const node = $.ui.fancytree.getNode(e); const node = $.ui.fancytree.getNode(e as unknown as Event);
const note = froca.getNoteFromCache(node.data.noteId); const note = froca.getNoteFromCache(node.data.noteId);
if (note.isLaunchBarConfig()) { if (note.isLaunchBarConfig()) {
@ -660,13 +721,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
prepareRootNode() { prepareRootNode() {
return this.prepareNode(froca.getBranch("none_root")); const branch = froca.getBranch("none_root");
return branch && this.prepareNode(branch);
} }
/** prepareChildren(parentNote: FNote) {
* @param {FNote} parentNote
*/
prepareChildren(parentNote) {
utils.assertArguments(parentNote); utils.assertArguments(parentNote);
const noteList = []; const noteList = [];
@ -697,7 +756,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return noteList; return noteList;
} }
async updateNode(node) { async updateNode(node: Fancytree.FancytreeNode) {
const note = froca.getNoteFromCache(node.data.noteId); const note = froca.getNoteFromCache(node.data.noteId);
const branch = froca.getBranch(node.data.branchId); const branch = froca.getBranch(node.data.branchId);
@ -725,11 +784,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
node.renderTitle(); node.renderTitle();
} }
/** prepareNode(branch: FBranch, forceLazy = false) {
* @param {FBranch} branch
* @param {boolean} forceLazy
*/
prepareNode(branch, forceLazy = false) {
const note = branch.getNoteFromCache(); const note = branch.getNoteFromCache();
if (!note) { if (!note) {
@ -741,7 +796,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const isFolder = note.isFolder(); const isFolder = note.isFolder();
const node = { const node: Node = {
noteId: note.noteId, noteId: note.noteId,
parentNoteId: branch.parentNoteId, parentNoteId: branch.parentNoteId,
branchId: branch.branchId, branchId: branch.branchId,
@ -749,7 +804,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
noteType: note.type, noteType: note.type,
title: utils.escapeHtml(title), title: utils.escapeHtml(title),
extraClasses: this.getExtraClasses(note), extraClasses: this.getExtraClasses(note),
icon: note.getIcon(isFolder), icon: note.getIcon(),
refKey: note.noteId, refKey: note.noteId,
lazy: true, lazy: true,
folder: isFolder, folder: isFolder,
@ -764,7 +819,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return node; return node;
} }
getExtraClasses(note) { getExtraClasses(note: FNote) {
utils.assertArguments(note); utils.assertArguments(note);
const extraClasses = []; const extraClasses = [];
@ -780,9 +835,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
if (note.getParentNoteIds().length > 1) { if (note.getParentNoteIds().length > 1) {
const realClones = note const realClones = note
.getParentNoteIds() .getParentNoteIds()
.map((noteId) => froca.notes[noteId]) .map((noteId: string) => froca.notes[noteId])
.filter((note) => !!note) .filter((note: FNote) => !!note)
.filter((note) => !["_share", "_lbBookmarks"].includes(note.noteId) && note.type !== "search"); .filter((note: FNote) => !["_share", "_lbBookmarks"].includes(note.noteId) && note.type !== "search");
if (realClones.length > 1) { if (realClones.length > 1) {
extraClasses.push("multiple-parents"); extraClasses.push("multiple-parents");
@ -820,8 +875,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return this.tree.getSelectedNodes(stopOnParents); return this.tree.getSelectedNodes(stopOnParents);
} }
/** @returns {FancytreeNode[]} */ getSelectedOrActiveNodes(node: Fancytree.FancytreeNode | null = null) {
getSelectedOrActiveNodes(node = null) {
const nodes = this.getSelectedNodes(true); const nodes = this.getSelectedNodes(true);
// the node you start dragging should be included even if not selected // the node you start dragging should be included even if not selected
@ -838,14 +892,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return nodes.filter((node) => hoistedNoteService.getHoistedNoteId() !== "root" || node.data.noteId !== "_hidden"); return nodes.filter((node) => hoistedNoteService.getHoistedNoteId() !== "root" || node.data.noteId !== "_hidden");
} }
async setExpandedStatusForSubtree(node, isExpanded) { async setExpandedStatusForSubtree(node: Fancytree.FancytreeNode | null, isExpanded: boolean) {
if (!node) { if (!node) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
node = this.getNodesByNoteId(hoistedNoteId)[0]; node = this.getNodesByNoteId(hoistedNoteId)[0];
} }
const { branchIds } = await server.put(`branches/${node.data.branchId}/expanded-subtree/${isExpanded ? 1 : 0}`); const { branchIds } = await server.put<ExpandedSubtreeResponse>(`branches/${node.data.branchId}/expanded-subtree/${isExpanded ? 1 : 0}`);
froca.getBranches(branchIds, true).forEach((branch) => (branch.isExpanded = !!isExpanded)); froca.getBranches(branchIds, true).forEach((branch) => (branch.isExpanded = !!isExpanded));
@ -863,11 +917,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// don't activate the active note, see discussion in https://github.com/zadam/trilium/issues/3664 // don't activate the active note, see discussion in https://github.com/zadam/trilium/issues/3664
} }
async expandTree(node = null) { async expandTree(node: Fancytree.FancytreeNode | null = null) {
await this.setExpandedStatusForSubtree(node, true); await this.setExpandedStatusForSubtree(node, true);
} }
async collapseTree(node = null) { async collapseTree(node: Fancytree.FancytreeNode | null = null) {
await this.setExpandedStatusForSubtree(node, false); await this.setExpandedStatusForSubtree(node, false);
} }
@ -918,8 +972,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.tree.setFocus(true); this.tree.setFocus(true);
} }
/** @returns {FancytreeNode} */ async getNodeFromPath(notePath: string, expand = false, logErrors = true) {
async getNodeFromPath(notePath, expand = false, logErrors = true) {
utils.assertArguments(notePath); utils.assertArguments(notePath);
/** @let {FancytreeNode} */ /** @let {FancytreeNode} */
let parentNode = this.getNodesByNoteId("root")[0]; let parentNode = this.getNodesByNoteId("root")[0];
@ -951,7 +1004,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// although the previous line should set the expanded status, it seems to happen asynchronously, // although the previous line should set the expanded status, it seems to happen asynchronously,
// so we need to make sure it is set properly before calling updateNode which uses this flag // so we need to make sure it is set properly before calling updateNode which uses this flag
const branch = froca.getBranch(parentNode.data.branchId); const branch = froca.getBranch(parentNode.data.branchId);
branch.isExpanded = true; if (branch) {
branch.isExpanded = true;
}
} }
await this.updateNode(parentNode); await this.updateNode(parentNode);
@ -990,25 +1045,25 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return parentNode; return parentNode;
} }
/** @returns {FancytreeNode} */ findChildNode(parentNode: Fancytree.FancytreeNode, childNoteId: string) {
findChildNode(parentNode, childNoteId) {
return parentNode.getChildren().find((childNode) => childNode.data.noteId === childNoteId); return parentNode.getChildren().find((childNode) => childNode.data.noteId === childNoteId);
} }
/** @returns {FancytreeNode} */ async expandToNote(notePath: string, logErrors = true) {
async expandToNote(notePath, logErrors = true) {
return this.getNodeFromPath(notePath, true, logErrors); return this.getNodeFromPath(notePath, true, logErrors);
} }
/** @returns {FancytreeNode[]} */ getNodesByBranch(branch: BranchRow) {
getNodesByBranch(branch) {
utils.assertArguments(branch); utils.assertArguments(branch);
if (!branch.noteId) {
return [];
}
return this.getNodesByNoteId(branch.noteId).filter((node) => node.data.branchId === branch.branchId); return this.getNodesByNoteId(branch.noteId).filter((node) => node.data.branchId === branch.branchId);
} }
/** @returns {FancytreeNode[]} */ getNodesByNoteId(noteId: string) {
getNodesByNoteId(noteId) {
utils.assertArguments(noteId); utils.assertArguments(noteId);
const list = this.tree.getNodesByRef(noteId); const list = this.tree.getNodesByRef(noteId);
@ -1043,7 +1098,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
if (newActiveNode) { if (newActiveNode) {
if (!newActiveNode.isVisible()) { if (!newActiveNode.isVisible() && this.noteContext?.notePath) {
await this.expandToNote(this.noteContext.notePath); await this.expandToNote(this.noteContext.notePath);
} }
@ -1055,8 +1110,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.filterHoistedBranch(false); this.filterHoistedBranch(false);
} }
async refreshSearch(e) { async refreshSearch(e: JQuery.MouseDownEvent) {
const activeNode = $.ui.fancytree.getNode(e); const activeNode = $.ui.fancytree.getNode(e as unknown as Event);
activeNode.load(true); activeNode.load(true);
activeNode.setExpanded(true, { noAnimation: true }); activeNode.setExpanded(true, { noAnimation: true });
@ -1064,7 +1119,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
toastService.showMessage(t("note_tree.saved-search-note-refreshed")); toastService.showMessage(t("note_tree.saved-search-note-refreshed"));
} }
async batchUpdate(cb) { async batchUpdate(cb: () => Promise<void>) {
try { try {
// disable rendering during update for increased performance // disable rendering during update for increased performance
this.tree.enableUpdate(false); this.tree.enableUpdate(false);
@ -1115,7 +1170,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}, 600 * 1000); }, 600 * 1000);
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
this.activityDetected(); this.activityDetected();
if (loadResults.isEmptyForTree()) { if (loadResults.isEmptyForTree()) {
@ -1126,14 +1181,15 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const activeNodeFocused = activeNode?.hasFocus(); const activeNodeFocused = activeNode?.hasFocus();
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const refreshCtx = { const refreshCtx: RefreshContext = {
noteIdsToUpdate: new Set(), noteIdsToUpdate: new Set(),
noteIdsToReload: new Set() noteIdsToReload: new Set()
}; };
this.#processAttributeRows(loadResults.getAttributeRows(), refreshCtx); this.#processAttributeRows(loadResults.getAttributeRows(), refreshCtx);
const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(loadResults.getBranchRows(), refreshCtx); const branchRows = loadResults.getBranchRows();
const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(branchRows, refreshCtx);
for (const noteId of loadResults.getNoteIds()) { for (const noteId of loadResults.getNoteIds()) {
refreshCtx.noteIdsToUpdate.add(noteId); refreshCtx.noteIdsToUpdate.add(noteId);
@ -1149,17 +1205,17 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
#processAttributeRows(attributeRows, refreshCtx) { #processAttributeRows(attributeRows: AttributeRow[], refreshCtx: RefreshContext) {
for (const attrRow of attributeRows) { for (const attrRow of attributeRows) {
const dirtyingLabels = ["iconClass", "cssClass", "workspace", "workspaceIconClass", "color"]; const dirtyingLabels = ["iconClass", "cssClass", "workspace", "workspaceIconClass", "color"];
if (attrRow.type === "label" && dirtyingLabels.includes(attrRow.name)) { if (attrRow.type === "label" && dirtyingLabels.includes(attrRow.name ?? "") && attrRow.noteId) {
if (attrRow.isInheritable) { if (attrRow.isInheritable) {
refreshCtx.noteIdsToReload.add(attrRow.noteId); refreshCtx.noteIdsToReload.add(attrRow.noteId);
} else { } else {
refreshCtx.noteIdsToUpdate.add(attrRow.noteId); refreshCtx.noteIdsToUpdate.add(attrRow.noteId);
} }
} else if (attrRow.type === "label" && attrRow.name === "archived") { } else if (attrRow.type === "label" && attrRow.name === "archived" && attrRow.noteId) {
const note = froca.getNoteFromCache(attrRow.noteId); const note = froca.getNoteFromCache(attrRow.noteId);
if (note) { if (note) {
@ -1169,13 +1225,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
refreshCtx.noteIdsToReload.add(parentNote.noteId); refreshCtx.noteIdsToReload.add(parentNote.noteId);
} }
} }
} else if (attrRow.type === "relation" && (attrRow.name === "template" || attrRow.name === "inherit")) { } else if (attrRow.type === "relation" && (attrRow.name === "template" || attrRow.name === "inherit") && attrRow.noteId) {
// missing handling of things inherited from template // missing handling of things inherited from template
refreshCtx.noteIdsToReload.add(attrRow.noteId); refreshCtx.noteIdsToReload.add(attrRow.noteId);
} else if (attrRow.type === "relation" && attrRow.name === "imageLink") { } else if (attrRow.type === "relation" && attrRow.name === "imageLink" && attrRow.noteId) {
const note = froca.getNoteFromCache(attrRow.noteId); const note = froca.getNoteFromCache(attrRow.noteId);
if (note && note.getChildNoteIds().includes(attrRow.value)) { if (note && note.getChildNoteIds().includes(attrRow.value ?? "")) {
// there's a new /deleted imageLink between note and its image child - which can show/hide // there's a new /deleted imageLink between note and its image child - which can show/hide
// the image (if there is an imageLink relation between parent and child, // the image (if there is an imageLink relation between parent and child,
// then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree) // then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree)
@ -1185,7 +1241,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
async #processBranchRows(branchRows, refreshCtx) { async #processBranchRows(branchRows: BranchRow[], refreshCtx: RefreshContext) {
const allBranchesDeleted = branchRows.every((branchRow) => !!branchRow.isDeleted); const allBranchesDeleted = branchRows.every((branchRow) => !!branchRow.isDeleted);
// activeNode is supposed to be moved when we find out activeNode is deleted but not all branches are deleted. save it for fixing activeNodePath after all nodes loaded. // activeNode is supposed to be moved when we find out activeNode is deleted but not all branches are deleted. save it for fixing activeNodePath after all nodes loaded.
@ -1193,12 +1249,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
let parentsOfAddedNodes = []; let parentsOfAddedNodes = [];
for (const branchRow of branchRows) { for (const branchRow of branchRows) {
if (branchRow.parentNoteId === "_share") { if (branchRow.noteId) {
// all shared notes have a sign in the tree, even the descendants of shared notes if (branchRow.parentNoteId === "_share") {
refreshCtx.noteIdsToReload.add(branchRow.noteId); // all shared notes have a sign in the tree, even the descendants of shared notes
} else { refreshCtx.noteIdsToReload.add(branchRow.noteId);
// adding noteId itself to update all potential clones } else {
refreshCtx.noteIdsToUpdate.add(branchRow.noteId); // adding noteId itself to update all potential clones
refreshCtx.noteIdsToUpdate.add(branchRow.noteId);
}
} }
if (branchRow.isDeleted) { if (branchRow.isDeleted) {
@ -1219,37 +1277,44 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
node.remove(); node.remove();
} }
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId); if (branchRow.parentNoteId) {
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId);
}
} }
} else { } else if (branchRow.parentNoteId) {
for (const parentNode of this.getNodesByNoteId(branchRow.parentNoteId)) { for (const parentNode of this.getNodesByNoteId(branchRow.parentNoteId)) {
parentsOfAddedNodes.push(parentNode); parentsOfAddedNodes.push(parentNode);
if (parentNode.isFolder() && !parentNode.isLoaded()) { if (!branchRow.noteId || (parentNode.isFolder() && !parentNode.isLoaded())) {
continue; continue;
} }
const note = await froca.getNote(branchRow.noteId); const note = await froca.getNote(branchRow.noteId);
const frocaBranch = froca.getBranch(branchRow.branchId); const frocaBranch = branchRow.branchId ? froca.getBranch(branchRow.branchId) : null;
const foundNode = (parentNode.getChildren() || []).find((child) => child.data.noteId === branchRow.noteId); const foundNode = (parentNode.getChildren() || []).find((child) => child.data.noteId === branchRow.noteId);
if (foundNode) { if (foundNode) {
// the branch already exists in the tree // the branch already exists in the tree
if (branchRow.isExpanded !== foundNode.isExpanded()) { if (branchRow.isExpanded !== foundNode.isExpanded() && frocaBranch) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId); refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
} }
} else { } else if (frocaBranch) {
// make sure it's loaded // make sure it's loaded
// we're forcing lazy since it's not clear if the whole required subtree is in froca // we're forcing lazy since it's not clear if the whole required subtree is in froca
parentNode.addChildren([this.prepareNode(frocaBranch, true)]); const newNode = this.prepareNode(frocaBranch, true);
if (newNode) {
parentNode.addChildren([newNode]);
}
if (frocaBranch.isExpanded && note.hasChildren()) { if (frocaBranch?.isExpanded && note && note.hasChildren()) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId); refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
} }
this.sortChildren(parentNode); this.sortChildren(parentNode);
// this might be a first child which would force an icon change // this might be a first child which would force an icon change
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId); if (branchRow.parentNoteId) {
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId);
}
} }
} }
} }
@ -1261,7 +1326,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}; };
} }
async #executeTreeUpdates(refreshCtx, loadResults) { async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) {
await this.batchUpdate(async () => { await this.batchUpdate(async () => {
for (const noteId of refreshCtx.noteIdsToReload) { for (const noteId of refreshCtx.noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) { for (const node of this.getNodesByNoteId(noteId)) {
@ -1288,7 +1353,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
async #setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes) { async #setActiveNode(activeNotePath: string | null, activeNodeFocused: boolean, movedActiveNode: Fancytree.FancytreeNode | null, parentsOfAddedNodes: Fancytree.FancytreeNode[]) {
if (movedActiveNode) { if (movedActiveNode) {
for (const parentNode of parentsOfAddedNodes) { for (const parentNode of parentsOfAddedNodes) {
const foundNode = (parentNode.getChildren() || []).find((child) => child.data.noteId === movedActiveNode.data.noteId); const foundNode = (parentNode.getChildren() || []).find((child) => child.data.noteId === movedActiveNode.data.noteId);
@ -1303,15 +1368,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return; return;
} }
let node = await this.expandToNote(activeNotePath, false); let node: Fancytree.FancytreeNode | null | undefined = await this.expandToNote(activeNotePath, false);
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) { if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) {
// if the active note has been moved elsewhere then it won't be found by the path, // if the active note has been moved elsewhere then it won't be found by the path,
// so we switch to the alternative of trying to find it by noteId // so we switch to the alternative of trying to find it by noteId
const notesById = this.getNodesByNoteId(treeService.getNoteIdFromUrl(activeNotePath)); const noteId = treeService.getNoteIdFromUrl(activeNotePath);
// if there are multiple clones, then we'd rather not activate anyone if (noteId) {
node = notesById.length === 1 ? notesById[0] : null; const notesById = this.getNodesByNoteId(noteId);
// if there are multiple clones, then we'd rather not activate anyone
node = notesById.length === 1 ? notesById[0] : null;
}
} }
if (!node) { if (!node) {
@ -1326,7 +1395,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
await node.setActive(true, { noEvents: true, noFocus: !activeNodeFocused }); await node.setActive(true, { noEvents: true, noFocus: !activeNodeFocused });
} }
sortChildren(node) { sortChildren(node: Fancytree.FancytreeNode) {
node.sortChildren((nodeA, nodeB) => { node.sortChildren((nodeA, nodeB) => {
const branchA = froca.branches[nodeA.data.branchId]; const branchA = froca.branches[nodeA.data.branchId];
const branchB = froca.branches[nodeB.data.branchId]; const branchB = froca.branches[nodeB.data.branchId];
@ -1339,7 +1408,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}); });
} }
setExpanded(branchId, isExpanded) { setExpanded(branchId: string, isExpanded: boolean) {
utils.assertArguments(branchId); utils.assertArguments(branchId);
const branch = froca.getBranch(branchId, true); const branch = froca.getBranch(branchId, true);
@ -1381,7 +1450,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
async hoistedNoteChangedEvent({ ntxId }) { async hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) {
if (this.isNoteContext(ntxId)) { if (this.isNoteContext(ntxId)) {
await this.filterHoistedBranch(true); await this.filterHoistedBranch(true);
} }
@ -1402,7 +1471,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.lastFilteredHoistedNotePath = hoistedNotePath; this.lastFilteredHoistedNotePath = hoistedNotePath;
await this.getNodeFromPath(hoistedNotePath); if (hoistedNotePath) {
await this.getNodeFromPath(hoistedNotePath);
}
if (this.noteContext.hoistedNoteId === "root") { if (this.noteContext.hoistedNoteId === "root") {
this.tree.clearFilter(); this.tree.clearFilter();
@ -1411,7 +1482,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// hack when hoisted note is cloned then it could be filtered multiple times while we want only 1 // hack when hoisted note is cloned then it could be filtered multiple times while we want only 1
this.tree.filterBranches( this.tree.filterBranches(
(node) => (node) =>
node.data.noteId === this.noteContext.hoistedNoteId && // optimization to not having always resolve the node path node.data.noteId === this.noteContext?.hoistedNoteId && // optimization to not having always resolve the node path
treeService.getNotePath(node) === hoistedNotePath treeService.getNotePath(node) === hoistedNotePath
); );
@ -1419,18 +1490,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
toggleHiddenNode(show) { toggleHiddenNode(show: boolean) {
const hiddenNode = this.getNodesByNoteId("_hidden")[0]; const hiddenNode = this.getNodesByNoteId("_hidden")[0];
$(hiddenNode.li).toggleClass("hidden-node-is-hidden", !show); // TODO: Check how .li exists here.
$((hiddenNode as any).li).toggleClass("hidden-node-is-hidden", !show);
} }
frocaReloadedEvent() { async frocaReloadedEvent() {
this.reloadTreeFromCache(); this.reloadTreeFromCache();
} }
async getHotKeys() { async getHotKeys() {
const actions = await keyboardActionsService.getActionsForScope("note-tree"); const actions = await keyboardActionsService.getActionsForScope("note-tree");
const hotKeyMap = {}; const hotKeyMap: Record<string, (node: Fancytree.FancytreeNode, e: JQuery.KeyDownEvent) => boolean> = {};
for (const action of actions) { for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) { for (const shortcut of action.effectiveShortcuts) {
@ -1447,25 +1519,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return hotKeyMap; return hotKeyMap;
} }
/** getSelectedOrActiveBranchIds(node: Fancytree.FancytreeNode) {
* @param {FancytreeNode} node
*/
getSelectedOrActiveBranchIds(node) {
const nodes = this.getSelectedOrActiveNodes(node); const nodes = this.getSelectedOrActiveNodes(node);
return nodes.map((node) => node.data.branchId); return nodes.map((node) => node.data.branchId);
} }
/** getSelectedOrActiveNoteIds(node: Fancytree.FancytreeNode): string[] {
* @param {FancytreeNode} node
*/
getSelectedOrActiveNoteIds(node) {
const nodes = this.getSelectedOrActiveNodes(node); const nodes = this.getSelectedOrActiveNodes(node);
return nodes.map((node) => node.data.noteId); return nodes.map((node) => node.data.noteId);
} }
async deleteNotesCommand({ node }) { async deleteNotesCommand({ node }: CommandListenerData<"deleteNotes">) {
const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-")); // search results can't be deleted const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-")); // search results can't be deleted
if (!branchIds.length) { if (!branchIds.length) {
@ -1477,7 +1543,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.clearSelectedNodes(); this.clearSelectedNodes();
} }
canBeMovedUpOrDown(node) { canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
if (node.data.noteId === "root") { if (node.data.noteId === "root") {
return false; return false;
} }
@ -1487,8 +1553,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return !parentNote?.hasLabel("sorted"); return !parentNote?.hasLabel("sorted");
} }
moveNoteUpCommand({ node }) { moveNoteUpCommand({ node }: CommandListenerData<"moveNoteUp">) {
if (!this.canBeMovedUpOrDown(node)) { if (!node || !this.canBeMovedUpOrDown(node)) {
return; return;
} }
@ -1499,7 +1565,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
moveNoteDownCommand({ node }) { moveNoteDownCommand({ node }: CommandListenerData<"moveNoteDown">) {
if (!this.canBeMovedUpOrDown(node)) { if (!this.canBeMovedUpOrDown(node)) {
return; return;
} }
@ -1511,11 +1577,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
moveNoteUpInHierarchyCommand({ node }) { moveNoteUpInHierarchyCommand({ node }: CommandListenerData<"moveNoteUpInHierarchy">) {
branchService.moveNodeUpInHierarchy(node); branchService.moveNodeUpInHierarchy(node);
} }
moveNoteDownInHierarchyCommand({ node }) { moveNoteDownInHierarchyCommand({ node }: CommandListenerData<"moveNoteDownInHierarchy">) {
const toNode = node.getPrevSibling(); const toNode = node.getPrevSibling();
if (toNode !== null) { if (toNode !== null) {
@ -1571,63 +1637,63 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
expandSubtreeCommand({ node }) { expandSubtreeCommand({ node }: CommandListenerData<"expandSubtree">) {
this.expandTree(node); this.expandTree(node);
} }
collapseSubtreeCommand({ node }) { collapseSubtreeCommand({ node }: CommandListenerData<"collapseSubtree">) {
this.collapseTree(node); this.collapseTree(node);
} }
async recentChangesInSubtreeCommand({ node }) { async recentChangesInSubtreeCommand({ node }: CommandListenerData<"recentChangesInSubtree">) {
this.triggerCommand("showRecentChanges", { ancestorNoteId: node.data.noteId }); this.triggerCommand("showRecentChanges", { ancestorNoteId: node.data.noteId });
} }
selectAllNotesInParentCommand({ node }) { selectAllNotesInParentCommand({ node }: CommandListenerData<"selectAllNotesInParent">) {
for (const child of node.getParent().getChildren()) { for (const child of node.getParent().getChildren()) {
child.setSelected(true); child.setSelected(true);
} }
} }
copyNotesToClipboardCommand({ node }) { copyNotesToClipboardCommand({ node }: CommandListenerData<"copyNotesToClipboard">) {
clipboard.copy(this.getSelectedOrActiveBranchIds(node)); clipboard.copy(this.getSelectedOrActiveBranchIds(node));
} }
cutNotesToClipboardCommand({ node }) { cutNotesToClipboardCommand({ node }: CommandListenerData<"cutNotesToClipboard">) {
clipboard.cut(this.getSelectedOrActiveBranchIds(node)); clipboard.cut(this.getSelectedOrActiveBranchIds(node));
} }
pasteNotesFromClipboardCommand({ node }) { pasteNotesFromClipboardCommand({ node }: CommandListenerData<"pasteNotesFromClipboard">) {
clipboard.pasteInto(node.data.branchId); clipboard.pasteInto(node.data.branchId);
} }
pasteNotesAfterFromClipboardCommand({ node }) { pasteNotesAfterFromClipboardCommand({ node }: CommandListenerData<"pasteNotesAfterFromClipboard">) {
clipboard.pasteAfter(node.data.branchId); clipboard.pasteAfter(node.data.branchId);
} }
async exportNoteCommand({ node }) { async exportNoteCommand({ node }: CommandListenerData<"exportNote">) {
const notePath = treeService.getNotePath(node); const notePath = treeService.getNotePath(node);
this.triggerCommand("showExportDialog", { notePath, defaultType: "subtree" }); this.triggerCommand("showExportDialog", { notePath, defaultType: "subtree" });
} }
async importIntoNoteCommand({ node }) { async importIntoNoteCommand({ node }: CommandListenerData<"importIntoNote">) {
this.triggerCommand("showImportDialog", { noteId: node.data.noteId }); this.triggerCommand("showImportDialog", { noteId: node.data.noteId });
} }
editNoteTitleCommand({ node }) { editNoteTitleCommand({ node }: CommandListenerData<"editNoteTitle">) {
appContext.triggerCommand("focusOnTitle"); appContext.triggerCommand("focusOnTitle");
} }
protectSubtreeCommand({ node }) { protectSubtreeCommand({ node }: CommandListenerData<"protectSubtree">) {
protectedSessionService.protectNote(node.data.noteId, true, true); protectedSessionService.protectNote(node.data.noteId, true, true);
} }
unprotectSubtreeCommand({ node }) { unprotectSubtreeCommand({ node }: CommandListenerData<"unprotectSubtree">) {
protectedSessionService.protectNote(node.data.noteId, false, true); protectedSessionService.protectNote(node.data.noteId, false, true);
} }
duplicateSubtreeCommand({ node }) { duplicateSubtreeCommand({ node }: CommandListenerData<"duplicateSubtree">) {
const nodesToDuplicate = this.getSelectedOrActiveNodes(node); const nodesToDuplicate = this.getSelectedOrActiveNodes(node);
for (const nodeToDuplicate of nodesToDuplicate) { for (const nodeToDuplicate of nodesToDuplicate) {
@ -1639,19 +1705,21 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const branch = froca.getBranch(nodeToDuplicate.data.branchId); const branch = froca.getBranch(nodeToDuplicate.data.branchId);
noteCreateService.duplicateSubtree(nodeToDuplicate.data.noteId, branch.parentNoteId); if (branch?.parentNoteId) {
noteCreateService.duplicateSubtree(nodeToDuplicate.data.noteId, branch.parentNoteId);
}
} }
} }
moveLauncherToVisibleCommand({ selectedOrActiveBranchIds }) { moveLauncherToVisibleCommand({ selectedOrActiveBranchIds }: CommandListenerData<"moveLauncherToVisible">) {
this.#moveLaunchers(selectedOrActiveBranchIds, "_lbVisibleLaunchers", "_lbMobileVisibleLaunchers"); this.#moveLaunchers(selectedOrActiveBranchIds, "_lbVisibleLaunchers", "_lbMobileVisibleLaunchers");
} }
moveLauncherToAvailableCommand({ selectedOrActiveBranchIds }) { moveLauncherToAvailableCommand({ selectedOrActiveBranchIds }: CommandListenerData<"moveLauncherToAvailable">) {
this.#moveLaunchers(selectedOrActiveBranchIds, "_lbAvailableLaunchers", "_lbMobileAvailableLaunchers"); this.#moveLaunchers(selectedOrActiveBranchIds, "_lbAvailableLaunchers", "_lbMobileAvailableLaunchers");
} }
#moveLaunchers(selectedOrActiveBranchIds, desktopParent, mobileParent) { #moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) {
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile")); const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
if (desktopLaunchersToMove) { if (desktopLaunchersToMove) {
branchService.moveToParentNote(desktopLaunchersToMove, "_lbRoot_" + desktopParent); branchService.moveToParentNote(desktopLaunchersToMove, "_lbRoot_" + desktopParent);
@ -1663,24 +1731,24 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
addNoteLauncherCommand({ node }) { addNoteLauncherCommand({ node }: CommandListenerData<"addNoteLauncher">) {
this.createLauncherNote(node, "note"); this.createLauncherNote(node, "note");
} }
addScriptLauncherCommand({ node }) { addScriptLauncherCommand({ node }: CommandListenerData<"addScriptLauncher">) {
this.createLauncherNote(node, "script"); this.createLauncherNote(node, "script");
} }
addWidgetLauncherCommand({ node }) { addWidgetLauncherCommand({ node }: CommandListenerData<"addWidgetLauncher">) {
this.createLauncherNote(node, "customWidget"); this.createLauncherNote(node, "customWidget");
} }
addSpacerLauncherCommand({ node }) { addSpacerLauncherCommand({ node }: CommandListenerData<"addSpacerLauncher">) {
this.createLauncherNote(node, "spacer"); this.createLauncherNote(node, "spacer");
} }
async createLauncherNote(node, launcherType) { async createLauncherNote(node: Fancytree.FancytreeNode, launcherType: LauncherType) {
const resp = await server.post(`special-notes/launchers/${node.data.noteId}/${launcherType}`); const resp = await server.post<CreateLauncherResponse>(`special-notes/launchers/${node.data.noteId}/${launcherType}`);
if (!resp.success) { if (!resp.success) {
toastService.showError(resp.message); toastService.showError(resp.message);

View File

@ -3,7 +3,7 @@
import dateNoteService from "../../services/date_notes.js"; import dateNoteService from "../../services/date_notes.js";
import sql from "../../services/sql.js"; import sql from "../../services/sql.js";
import cls from "../../services/cls.js"; import cls from "../../services/cls.js";
import specialNotesService from "../../services/special_notes.js"; import specialNotesService, { type LauncherType } from "../../services/special_notes.js";
import becca from "../../becca/becca.js"; import becca from "../../becca/becca.js";
import type { Request } from "express"; import type { Request } from "express";
@ -85,7 +85,8 @@ function getHoistedNote() {
function createLauncher(req: Request) { function createLauncher(req: Request) {
return specialNotesService.createLauncher({ return specialNotesService.createLauncher({
parentNoteId: req.params.parentNoteId, parentNoteId: req.params.parentNoteId,
launcherType: req.params.launcherType // TODO: Validate the parameter
launcherType: req.params.launcherType as LauncherType
}); });
} }

View File

@ -156,9 +156,11 @@ function createScriptLauncher(parentNoteId: string, forceNoteId?: string) {
return note; return note;
} }
export type LauncherType = "launcher" | "note" | "script" | "customWidget" | "spacer";
interface LauncherConfig { interface LauncherConfig {
parentNoteId: string; parentNoteId: string;
launcherType: string; launcherType: LauncherType;
noteId?: string; noteId?: string;
} }