fix(server): in-app help not integrated

This commit is contained in:
Elian Doran
2026-04-09 16:37:13 +03:00
parent d34ba8b6f3
commit 66a18d12dc
9 changed files with 206 additions and 172 deletions

View File

@@ -1,7 +1,7 @@
import { getLog, initializeCore, sql_init } from "@triliumnext/core";
import ClsHookedExecutionContext from "@triliumnext/server/src/cls_provider.js";
import NodejsCryptoProvider from "@triliumnext/server/src/crypto_provider.js";
import NodejsZipProvider from "@triliumnext/server/src/zip_provider.js";
import NodejsInAppHelpProvider from "@triliumnext/server/src/in_app_help_provider.js";
import dataDirs from "@triliumnext/server/src/services/data_dir.js";
import options from "@triliumnext/server/src/services/options.js";
import port from "@triliumnext/server/src/services/port.js";
@@ -10,6 +10,7 @@ import tray from "@triliumnext/server/src/services/tray.js";
import windowService from "@triliumnext/server/src/services/window.js";
import WebSocketMessagingProvider from "@triliumnext/server/src/services/ws_messaging_provider.js";
import BetterSqlite3Provider from "@triliumnext/server/src/sql_provider.js";
import NodejsZipProvider from "@triliumnext/server/src/zip_provider.js";
import { app, BrowserWindow,globalShortcut } from "electron";
import electronDebug from "electron-debug";
import electronDl from "electron-dl";
@@ -143,6 +144,7 @@ async function main() {
platform: new DesktopPlatformProvider(),
translations: (await import("@triliumnext/server/src/services/i18n.js")).initializeTranslations,
getDemoArchive: async () => fs.readFileSync(require.resolve("@triliumnext/server/src/assets/db/demo.zip")),
inAppHelp: new NodejsInAppHelpProvider(),
extraAppInfo: {
nodeVersion: process.version,
dataDirectory: path.resolve(dataDirs.TRILIUM_DATA_DIR)

View File

@@ -1,7 +1,7 @@
import debounce from "@triliumnext/client/src/services/debounce.js";
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/core";
import NodejsInAppHelpProvider from "@triliumnext/server/src/in_app_help_provider.js";
import cls from "@triliumnext/server/src/services/cls.js";
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
import type { NoteMetaFile } from "@triliumnext/server/src/services/meta/note_meta.js";
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
import fs from "fs/promises";
@@ -241,7 +241,7 @@ async function cleanUpMeta(outputPath: string, minify: boolean) {
}
if (minify) {
const subtree = parseNoteMetaFile(meta);
const subtree = new NodejsInAppHelpProvider().parseNoteMetaFile(meta);
await fs.writeFile(metaPath, JSON.stringify(subtree));
} else {
await fs.writeFile(metaPath, JSON.stringify(meta, null, 4));

View File

@@ -8,6 +8,7 @@ import NodejsCryptoProvider from "../src/crypto_provider.js";
import NodejsZipProvider from "../src/zip_provider.js";
import ServerPlatformProvider from "../src/platform_provider.js";
import BetterSqlite3Provider from "../src/sql_provider.js";
import NodejsInAppHelpProvider from "../src/in_app_help_provider.js";
import { initializeTranslations } from "../src/services/i18n.js";
// Initialize environment variables.
@@ -39,6 +40,7 @@ beforeAll(async () => {
executionContext: new ClsHookedExecutionContext(),
schema: readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
platform: new ServerPlatformProvider(),
translations: initializeTranslations
translations: initializeTranslations,
inAppHelp: new NodejsInAppHelpProvider()
});
});

View File

@@ -0,0 +1,165 @@
import type { HiddenSubtreeItem } from "@triliumnext/commons";
import type { InAppHelpProvider } from "@triliumnext/core";
import fs from "fs";
import path from "path";
import becca from "./becca/becca.js";
import type BNote from "./becca/entities/bnote.js";
import type NoteMeta from "./services/meta/note_meta.js";
import type { NoteMetaFile } from "./services/meta/note_meta.js";
import { RESOURCE_DIR } from "./services/resource_dir.js";
export default class NodejsInAppHelpProvider implements InAppHelpProvider {
getHelpHiddenSubtreeData(): HiddenSubtreeItem[] {
const helpDir = path.join(RESOURCE_DIR, "doc_notes", "en", "User Guide");
const metaFilePath = path.join(helpDir, "!!!meta.json");
try {
return JSON.parse(fs.readFileSync(metaFilePath).toString("utf-8"));
} catch (e) {
console.warn(e);
return [];
}
}
parseNoteMetaFile(noteMetaFile: NoteMetaFile): HiddenSubtreeItem[] {
if (!noteMetaFile.files) {
console.log("No meta files");
return [];
}
const metaRoot = noteMetaFile.files[0];
const parsedMetaRoot = this.parseNoteMeta(metaRoot, "/" + (metaRoot.dirFileName ?? ""));
return parsedMetaRoot?.children ?? [];
}
parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeItem | null {
let iconClass: string = "bx bx-file";
const item: HiddenSubtreeItem = {
id: `_help_${noteMeta.noteId}`,
title: noteMeta.title ?? "",
type: "doc", // can change
attributes: []
};
// Handle folder notes
if (!noteMeta.dataFileName) {
iconClass = "bx bx-folder";
item.type = "book";
}
// Handle attributes
for (const attribute of noteMeta.attributes ?? []) {
if (attribute.name === "iconClass") {
iconClass = attribute.value;
continue;
}
if (attribute.name === "webViewSrc") {
item.attributes?.push({
type: "label",
name: attribute.name,
value: attribute.value
});
}
if (attribute.name === "shareHiddenFromTree") {
return null;
}
}
// Handle text notes
if (noteMeta.type === "text" && noteMeta.dataFileName) {
const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`.substring(1);
item.attributes?.push({
type: "label",
name: "docName",
value: docPath
});
}
// Handle web views
if (noteMeta.type === "webView") {
item.type = "webView";
item.enforceAttributes = true;
}
// Handle children
if (noteMeta.children) {
const children: HiddenSubtreeItem[] = [];
for (const childMeta of noteMeta.children) {
let newDocNameRoot = noteMeta.dirFileName ? `${docNameRoot}/${noteMeta.dirFileName}` : docNameRoot;
const item = this.parseNoteMeta(childMeta, newDocNameRoot);
if (item) {
children.push(item);
}
}
item.children = children;
}
// Handle note icon
item.attributes?.push({
name: "iconClass",
value: iconClass,
type: "label"
});
return item;
}
/**
* Iterates recursively through the help subtree that the user has and compares it against the definition
* to remove any notes that are no longer present in the latest version of the help.
*
* @param helpDefinition the hidden subtree definition for the help, to compare against the user's structure.
*/
cleanUpHelp(helpDefinition: HiddenSubtreeItem[]): void {
function getFlatIds(items: HiddenSubtreeItem | HiddenSubtreeItem[]) {
const ids: (string | string[])[] = [];
if (Array.isArray(items)) {
for (const item of items) {
ids.push(getFlatIds(item));
}
} else {
if (items.children) {
for (const child of items.children) {
ids.push(getFlatIds(child));
}
}
ids.push(items.id);
}
return ids.flat();
}
function getFlatIdsFromNote(note: BNote | null) {
if (!note) {
return [];
}
const ids: (string | string[])[] = [];
for (const subnote of note.getChildNotes()) {
ids.push(getFlatIdsFromNote(subnote));
}
ids.push(note.noteId);
return ids.flat();
}
const definitionHelpIds = new Set(getFlatIds(helpDefinition));
const realHelpIds = getFlatIdsFromNote(becca.getNote("_help"));
for (const realHelpId of realHelpIds) {
if (realHelpId === "_help") {
continue;
}
if (!definitionHelpIds.has(realHelpId)) {
becca.getNote(realHelpId)?.deleteNote();
}
}
}
}

View File

@@ -10,6 +10,7 @@ import path from "path";
import ClsHookedExecutionContext from "./cls_provider.js";
import NodejsCryptoProvider from "./crypto_provider.js";
import NodejsInAppHelpProvider from "./in_app_help_provider.js";
import ServerPlatformProvider from "./platform_provider.js";
import dataDirs from "./services/data_dir.js";
import port from "./services/port.js";
@@ -61,6 +62,7 @@ async function startApplication() {
platform: new ServerPlatformProvider(),
translations: (await import("./services/i18n.js")).initializeTranslations,
getDemoArchive: async () => fs.readFileSync(require.resolve("@triliumnext/server/src/assets/db/demo.zip")),
inAppHelp: new NodejsInAppHelpProvider(),
extraAppInfo: {
nodeVersion: process.version,
dataDirectory: path.resolve(dataDirs.TRILIUM_DATA_DIR)

View File

@@ -1,7 +1,10 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { parseNoteMeta } from "./in_app_help.js";
import { describe, expect, it } from "vitest";
import NodejsInAppHelpProvider from "../in_app_help_provider.js";
import type NoteMeta from "./meta/note_meta.js";
const provider = new NodejsInAppHelpProvider();
describe("In-app help", () => {
it("preserves custom folder icon", () => {
const meta: NoteMeta = {
@@ -29,7 +32,7 @@ describe("In-app help", () => {
children: []
};
const item = parseNoteMeta(meta, "/");
const item = provider.parseNoteMeta(meta, "/");
const icon = item?.attributes?.find((a) => a.name === "iconClass");
expect(icon?.value).toBe("bx bx-star");
});
@@ -60,7 +63,7 @@ describe("In-app help", () => {
children: []
};
const item = parseNoteMeta(meta, "/");
const item = provider.parseNoteMeta(meta, "/");
expect(item).toBeFalsy();
});
});

View File

@@ -1,158 +0,0 @@
import path from "path";
import fs from "fs";
import type NoteMeta from "./meta/note_meta.js";
import type { NoteMetaFile } from "./meta/note_meta.js";
import type BNote from "../becca/entities/bnote.js";
import becca from "../becca/becca.js";
import type { HiddenSubtreeItem } from "@triliumnext/commons";
import { RESOURCE_DIR } from "./resource_dir.js";
export function getHelpHiddenSubtreeData() {
const helpDir = path.join(RESOURCE_DIR, "doc_notes", "en", "User Guide");
const metaFilePath = path.join(helpDir, "!!!meta.json");
try {
return JSON.parse(fs.readFileSync(metaFilePath).toString("utf-8"));
} catch (e) {
console.warn(e);
return [];
}
}
export function parseNoteMetaFile(noteMetaFile: NoteMetaFile): HiddenSubtreeItem[] {
if (!noteMetaFile.files) {
console.log("No meta files");
return [];
}
const metaRoot = noteMetaFile.files[0];
const parsedMetaRoot = parseNoteMeta(metaRoot, "/" + (metaRoot.dirFileName ?? ""));
return parsedMetaRoot?.children ?? [];
}
export function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeItem | null {
let iconClass: string = "bx bx-file";
const item: HiddenSubtreeItem = {
id: `_help_${noteMeta.noteId}`,
title: noteMeta.title ?? "",
type: "doc", // can change
attributes: []
};
// Handle folder notes
if (!noteMeta.dataFileName) {
iconClass = "bx bx-folder";
item.type = "book";
}
// Handle attributes
for (const attribute of noteMeta.attributes ?? []) {
if (attribute.name === "iconClass") {
iconClass = attribute.value;
continue;
}
if (attribute.name === "webViewSrc") {
item.attributes?.push({
type: "label",
name: attribute.name,
value: attribute.value
});
}
if (attribute.name === "shareHiddenFromTree") {
return null;
}
}
// Handle text notes
if (noteMeta.type === "text" && noteMeta.dataFileName) {
const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`.substring(1);
item.attributes?.push({
type: "label",
name: "docName",
value: docPath
});
}
// Handle web views
if (noteMeta.type === "webView") {
item.type = "webView";
item.enforceAttributes = true;
}
// Handle children
if (noteMeta.children) {
const children: HiddenSubtreeItem[] = [];
for (const childMeta of noteMeta.children) {
let newDocNameRoot = noteMeta.dirFileName ? `${docNameRoot}/${noteMeta.dirFileName}` : docNameRoot;
const item = parseNoteMeta(childMeta, newDocNameRoot);
if (item) {
children.push(item);
}
}
item.children = children;
}
// Handle note icon
item.attributes?.push({
name: "iconClass",
value: iconClass,
type: "label"
});
return item;
}
/**
* Iterates recursively through the help subtree that the user has and compares it against the definition
* to remove any notes that are no longer present in the latest version of the help.
*
* @param helpDefinition the hidden subtree definition for the help, to compare against the user's structure.
*/
export function cleanUpHelp(helpDefinition: HiddenSubtreeItem[]) {
function getFlatIds(items: HiddenSubtreeItem | HiddenSubtreeItem[]) {
const ids: (string | string[])[] = [];
if (Array.isArray(items)) {
for (const item of items) {
ids.push(getFlatIds(item));
}
} else {
if (items.children) {
for (const child of items.children) {
ids.push(getFlatIds(child));
}
}
ids.push(items.id);
}
return ids.flat();
}
function getFlatIdsFromNote(note: BNote | null) {
if (!note) {
return [];
}
const ids: (string | string[])[] = [];
for (const subnote of note.getChildNotes()) {
ids.push(getFlatIdsFromNote(subnote));
}
ids.push(note.noteId);
return ids.flat();
}
const definitionHelpIds = new Set(getFlatIds(helpDefinition));
const realHelpIds = getFlatIdsFromNote(becca.getNote("_help"));
for (const realHelpId of realHelpIds) {
if (realHelpId === "_help") {
continue;
}
if (!definitionHelpIds.has(realHelpId)) {
becca.getNote(realHelpId)?.deleteNote();
}
}
}

View File

@@ -11,6 +11,7 @@ import appInfo from "./services/app_info";
import { type PlatformProvider, initPlatform } from "./services/platform";
import { type ZipProvider, initZipProvider } from "./services/zip_provider";
import { type ZipExportProviderFactory, initZipExportProviderFactory } from "./services/export/zip_export_provider_factory";
import { type InAppHelpProvider, initInAppHelp } from "./services/in_app_help";
export { getLog } from "./services/log";
export type * from "./services/sql/types";
@@ -96,6 +97,7 @@ export { default as content_hash } from "./services/content_hash";
export { default as sync_mutex } from "./services/sync_mutex";
export { default as setup } from "./services/setup";
export { getPlatform, type PlatformProvider } from "./services/platform";
export type { InAppHelpProvider } from "./services/in_app_help";
export { t } from "i18next";
export type { RequestProvider, ExecOpts, CookieJar } from "./services/request";
export type * from "./meta";
@@ -119,7 +121,7 @@ export { default as scriptService } from "./services/script";
export { default as BackendScriptApi, type Api as BackendScriptApiInterface } from "./services/backend_script_api";
export * as scheduler from "./services/scheduler";
export async function initializeCore({ dbConfig, executionContext, crypto, zip, zipExportProviderFactory, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive }: {
export async function initializeCore({ dbConfig, executionContext, crypto, zip, zipExportProviderFactory, translations, messaging, request, schema, extraAppInfo, platform, getDemoArchive, inAppHelp }: {
dbConfig: SqlServiceParams,
executionContext: ExecutionContext,
crypto: CryptoProvider,
@@ -131,6 +133,7 @@ export async function initializeCore({ dbConfig, executionContext, crypto, zip,
messaging?: MessagingProvider,
request?: RequestProvider,
getDemoArchive?: () => Promise<Uint8Array | null>,
inAppHelp?: InAppHelpProvider,
extraAppInfo?: {
nodeVersion: string;
dataDirectory: string;
@@ -155,4 +158,7 @@ export async function initializeCore({ dbConfig, executionContext, crypto, zip,
if (request) {
initRequest(request);
}
if (inAppHelp) {
initInAppHelp(inAppHelp);
}
};

View File

@@ -1,8 +1,20 @@
export function cleanUpHelp(items: unknown[]) {
// TODO: implement.
import type { HiddenSubtreeItem } from "@triliumnext/commons";
export interface InAppHelpProvider {
getHelpHiddenSubtreeData(): HiddenSubtreeItem[];
cleanUpHelp(items: HiddenSubtreeItem[]): void;
}
export function getHelpHiddenSubtreeData() {
// TODO: implement.
return [];
let provider: InAppHelpProvider | null = null;
export function initInAppHelp(p: InAppHelpProvider) {
provider = p;
}
export function getHelpHiddenSubtreeData(): HiddenSubtreeItem[] {
return provider?.getHelpHiddenSubtreeData() ?? [];
}
export function cleanUpHelp(items: HiddenSubtreeItem[]): void {
provider?.cleanUpHelp(items);
}