Notes/src/services/hidden_subtree.ts
2025-02-22 12:31:39 +02:00

437 lines
18 KiB
TypeScript

import BAttribute from "../becca/entities/battribute.js";
import type { AttributeType, NoteType } from "../becca/entities/rows.js";
import becca from "../becca/becca.js";
import noteService from "./notes.js";
import log from "./log.js";
import migrationService from "./migration.js";
import { t } from "i18next";
import { cleanUpHelp, getHelpHiddenSubtreeData } from "./in_app_help.js";
import buildLaunchBarConfig from "./hidden_subtree_launcherbar.js";
const LBTPL_ROOT = "_lbTplRoot";
const LBTPL_BASE = "_lbTplBase";
const LBTPL_COMMAND = "_lbTplCommandLauncher";
const LBTPL_NOTE_LAUNCHER = "_lbTplNoteLauncher";
const LBTPL_SCRIPT = "_lbTplScriptLauncher";
const LBTPL_BUILTIN_WIDGET = "_lbTplBuiltinWidget";
const LBTPL_SPACER = "_lbTplSpacer";
const LBTPL_CUSTOM_WIDGET = "_lbTplCustomWidget";
interface HiddenSubtreeAttribute {
type: AttributeType;
name: string;
isInheritable?: boolean;
value?: string;
}
export interface HiddenSubtreeItem {
notePosition?: number;
id: string;
title: string;
type: NoteType;
icon?: string;
attributes?: HiddenSubtreeAttribute[];
children?: HiddenSubtreeItem[];
isExpanded?: boolean;
baseSize?: string;
growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar" | "quickSearch";
command?: keyof typeof Command;
}
// TODO: Move this into a commons project once the client/server architecture is well split.
enum Command {
jumpToNote,
searchNotes,
createNoteIntoInbox,
showRecentChanges,
showOptions
}
/*
* Hidden subtree is generated as a "predictable structure" which means that it avoids generating random IDs to always
* produce the same structure. This is needed because it is run on multiple instances in the sync cluster which might produce
* duplicate subtrees. This way, all instances will generate the same structure with the same IDs.
*/
let hiddenSubtreeDefinition: HiddenSubtreeItem;
function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenSubtreeItem {
const launchbarConfig = buildLaunchBarConfig();
return {
id: "_hidden",
title: t("hidden-subtree.root-title"),
type: "doc",
icon: "bx bx-hide",
// we want to keep the hidden subtree always last, otherwise there will be problems with e.g., keyboard navigation
// over tree when it's in the middle
notePosition: 999_999_999,
attributes: [
{ type: "label", name: "excludeFromNoteMap", isInheritable: true },
{ type: "label", name: "docName", value: "hidden" }
],
children: [
{
id: "_search",
title: t("hidden-subtree.search-history-title"),
type: "doc"
},
{
id: "_globalNoteMap",
title: t("hidden-subtree.note-map-title"),
type: "noteMap",
attributes: [
{ type: "label", name: "mapRootNoteId", value: "hoisted" },
{ type: "label", name: "keepCurrentHoisting" }
]
},
{
id: "_sqlConsole",
title: t("hidden-subtree.sql-console-history-title"),
type: "doc",
icon: "bx-data"
},
{
id: "_share",
title: t("hidden-subtree.shared-notes-title"),
type: "doc",
attributes: [{ type: "label", name: "docName", value: "share" }]
},
{
id: "_bulkAction",
title: t("hidden-subtree.bulk-action-title"),
type: "doc"
},
{
id: "_backendLog",
title: t("hidden-subtree.backend-log-title"),
type: "contentWidget",
icon: "bx-terminal",
attributes: [
{ type: "label", name: "keepCurrentHoisting" },
{ type: "label", name: "fullContentWidth" }
]
},
{
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
id: "_userHidden",
title: t("hidden-subtree.user-hidden-title"),
type: "doc",
attributes: [{ type: "label", name: "docName", value: "user_hidden" }]
},
{
id: LBTPL_ROOT,
title: t("hidden-subtree.launch-bar-templates-title"),
type: "doc",
children: [
{
id: LBTPL_BASE,
title: t("hidden-subtree.base-abstract-launcher-title"),
type: "doc"
},
{
id: LBTPL_COMMAND,
title: t("hidden-subtree.command-launcher-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "command" },
{ type: "label", name: "docName", value: "launchbar_command_launcher" }
]
},
{
id: LBTPL_NOTE_LAUNCHER,
title: t("hidden-subtree.note-launcher-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "note" },
{ type: "label", name: "relation:target", value: "promoted" },
{ type: "label", name: "relation:hoistedNote", value: "promoted" },
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
{ type: "label", name: "docName", value: "launchbar_note_launcher" }
]
},
{
id: LBTPL_SCRIPT,
title: t("hidden-subtree.script-launcher-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "script" },
{ type: "label", name: "relation:script", value: "promoted" },
{ type: "label", name: "label:keyboardShortcut", value: "promoted,text" },
{ type: "label", name: "docName", value: "launchbar_script_launcher" }
]
},
{
id: LBTPL_BUILTIN_WIDGET,
title: t("hidden-subtree.built-in-widget-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "builtinWidget" }
]
},
{
id: LBTPL_SPACER,
title: t("hidden-subtree.spacer-title"),
type: "doc",
icon: "bx-move-vertical",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET },
{ type: "label", name: "builtinWidget", value: "spacer" },
{ type: "label", name: "label:baseSize", value: "promoted,number" },
{ type: "label", name: "label:growthFactor", value: "promoted,number" },
{ type: "label", name: "docName", value: "launchbar_spacer" }
]
},
{
id: LBTPL_CUSTOM_WIDGET,
title: t("hidden-subtree.custom-widget-title"),
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "customWidget" },
{ type: "label", name: "relation:widget", value: "promoted" },
{ type: "label", name: "docName", value: "launchbar_widget_launcher" }
]
}
]
},
{
id: "_lbRoot",
title: t("hidden-subtree.launch-bar-title"),
type: "doc",
icon: "bx-sidebar",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbAvailableLaunchers",
title: t("hidden-subtree.available-launchers-title"),
type: "doc",
icon: "bx-hide",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.desktopAvailableLaunchers
},
{
id: "_lbVisibleLaunchers",
title: t("hidden-subtree.visible-launchers-title"),
type: "doc",
icon: "bx-show",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.desktopVisibleLaunchers
}
]
},
{
id: "_lbMobileRoot",
title: "Mobile Launch Bar",
type: "doc",
icon: "bx-mobile",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbMobileAvailableLaunchers",
title: t("hidden-subtree.available-launchers-title"),
type: "doc",
icon: "bx-hide",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.mobileAvailableLaunchers
},
{
id: "_lbMobileVisibleLaunchers",
title: t("hidden-subtree.visible-launchers-title"),
type: "doc",
icon: "bx-show",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: launchbarConfig.mobileVisibleLaunchers
}
]
},
{
id: "_options",
title: t("hidden-subtree.options-title"),
type: "book",
icon: "bx-cog",
children: [
{ id: "_optionsAppearance", title: t("hidden-subtree.appearance-title"), type: "contentWidget", icon: "bx-layout" },
{ id: "_optionsShortcuts", title: t("hidden-subtree.shortcuts-title"), type: "contentWidget", icon: "bxs-keyboard" },
{ id: "_optionsTextNotes", title: t("hidden-subtree.text-notes"), type: "contentWidget", icon: "bx-text" },
{ id: "_optionsCodeNotes", title: t("hidden-subtree.code-notes-title"), type: "contentWidget", icon: "bx-code" },
{ id: "_optionsImages", title: t("hidden-subtree.images-title"), type: "contentWidget", icon: "bx-image" },
{ id: "_optionsSpellcheck", title: t("hidden-subtree.spellcheck-title"), type: "contentWidget", icon: "bx-check-double" },
{ id: "_optionsPassword", title: t("hidden-subtree.password-title"), type: "contentWidget", icon: "bx-lock" },
{ id: "_optionsEtapi", title: t("hidden-subtree.etapi-title"), type: "contentWidget", icon: "bx-extension" },
{ id: "_optionsBackup", title: t("hidden-subtree.backup-title"), type: "contentWidget", icon: "bx-data" },
{ id: "_optionsSync", title: t("hidden-subtree.sync-title"), type: "contentWidget", icon: "bx-wifi" },
{ id: "_optionsOther", title: t("hidden-subtree.other"), type: "contentWidget", icon: "bx-dots-horizontal" },
{ id: "_optionsAdvanced", title: t("hidden-subtree.advanced-title"), type: "contentWidget" }
]
},
{
id: "_help",
title: t("hidden-subtree.user-guide"),
type: "book",
icon: "bx-help-circle",
children: helpSubtree,
isExpanded: true
}
]
};
}
interface CheckHiddenExtraOpts {
restoreNames?: boolean;
}
function checkHiddenSubtree(force = false, extraOpts: CheckHiddenExtraOpts = {}) {
if (!force && !migrationService.isDbUpToDate()) {
// on-delete hook might get triggered during some future migration and cause havoc
log.info("Will not check hidden subtree until migration is finished.");
return;
}
const helpSubtree = getHelpHiddenSubtreeData();
if (!hiddenSubtreeDefinition || force) {
hiddenSubtreeDefinition = buildHiddenSubtreeDefinition(helpSubtree);
}
checkHiddenSubtreeRecursively("root", hiddenSubtreeDefinition, extraOpts);
try {
cleanUpHelp(helpSubtree);
} catch (e) {
// Non-critical operation should something go wrong.
console.error(e);
}
}
function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtreeItem, extraOpts: CheckHiddenExtraOpts = {}) {
if (!item.id || !item.type || !item.title) {
throw new Error(`Item does not contain mandatory properties: ${JSON.stringify(item)}`);
}
if (item.id.charAt(0) !== "_") {
throw new Error(`ID has to start with underscore, given '${item.id}'`);
}
let note = becca.notes[item.id];
let branch;
if (!note) {
({ note, branch } = noteService.createNewNote({
noteId: item.id,
title: item.title,
type: item.type,
parentNoteId: parentNoteId,
content: "",
ignoreForbiddenParents: true
}));
} else {
branch = note.getParentBranches().find((branch) => branch.parentNoteId === parentNoteId);
}
const attrs = [...(item.attributes || [])];
if (item.icon) {
attrs.push({ type: "label", name: "iconClass", value: `bx ${item.icon}` });
}
if (item.type === "launcher") {
if (item.command) {
attrs.push({ type: "relation", name: "template", value: LBTPL_COMMAND });
attrs.push({ type: "label", name: "command", value: item.command });
} else if (item.builtinWidget) {
if (item.builtinWidget === "spacer") {
attrs.push({ type: "relation", name: "template", value: LBTPL_SPACER });
attrs.push({ type: "label", name: "baseSize", value: item.baseSize });
attrs.push({ type: "label", name: "growthFactor", value: item.growthFactor });
} else {
attrs.push({ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET });
}
attrs.push({ type: "label", name: "builtinWidget", value: item.builtinWidget });
} else if (item.targetNoteId) {
attrs.push({ type: "relation", name: "template", value: LBTPL_NOTE_LAUNCHER });
attrs.push({ type: "relation", name: "target", value: item.targetNoteId });
} else {
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
}
}
if (extraOpts.restoreNames && note.title !== item.title) {
note.title = item.title;
note.save();
}
if (note.type !== item.type) {
// enforce a correct note type
note.type = item.type;
note.save();
}
if (branch) {
// in case of launchers the branch ID is not preserved and should not be relied upon - launchers which move between
// visible and available will change branch since the branch's parent-child relationship is immutable
if (item.notePosition !== undefined && branch.notePosition !== item.notePosition) {
branch.notePosition = item.notePosition;
branch.save();
}
if (item.isExpanded !== undefined && branch.isExpanded !== item.isExpanded) {
branch.isExpanded = item.isExpanded;
branch.save();
}
}
for (const attr of attrs) {
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
const existingAttribute = note.getAttributes().find((attr) => attr.attributeId === attrId);
if (!existingAttribute) {
new BAttribute({
attributeId: attrId,
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: false
}).save();
} else if (attr.name === "docName"
|| (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
if (existingAttribute.value !== attr.value) {
existingAttribute.value = attr.value ?? "";
console.log("Updating attribute ", attrId);
existingAttribute.save();
}
}
}
for (const child of item.children || []) {
checkHiddenSubtreeRecursively(item.id, child, extraOpts);
}
}
export default {
checkHiddenSubtree,
LBTPL_ROOT,
LBTPL_BASE,
LBTPL_COMMAND,
LBTPL_NOTE_LAUNCHER,
LBTPL_SCRIPT,
LBTPL_BUILTIN_WIDGET,
LBTPL_SPACER,
LBTPL_CUSTOM_WIDGET
};