mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 03:27:20 +02:00
fix(server): in-app help not integrated
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
});
|
||||
|
||||
165
apps/server/src/in_app_help_provider.ts
Normal file
165
apps/server/src/in_app_help_provider.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user