mirror of
https://github.com/zadam/trilium.git
synced 2025-11-03 11:56:01 +01:00
Compare commits
3 Commits
fix/resolv
...
fix/resolv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72dbf7f31d | ||
|
|
755254d037 | ||
|
|
7963f03e71 |
@@ -35,7 +35,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.3.2",
|
||||
"i18next": "25.3.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-hotkeys": "0.2.2",
|
||||
|
||||
@@ -81,7 +81,7 @@ let rootCreationDate: Date | undefined;
|
||||
async function getNoteTypeItems(command?: TreeCommandNames) {
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
...getBlankNoteTypes(command),
|
||||
...await getBuiltInTemplates(t("note_types.collections"), command, true),
|
||||
...await getBuiltInTemplates("Collections", command, true),
|
||||
...await getBuiltInTemplates(null, command, false),
|
||||
...await getUserTemplates(command)
|
||||
];
|
||||
@@ -89,28 +89,26 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
|
||||
return items;
|
||||
}
|
||||
|
||||
function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandNames>[] {
|
||||
return NOTE_TYPES
|
||||
.filter((nt) => !nt.reserved && nt.type !== "book")
|
||||
.map((nt) => {
|
||||
const menuItem: MenuCommandItem<TreeCommandNames> = {
|
||||
title: nt.title,
|
||||
command,
|
||||
type: nt.type,
|
||||
uiIcon: "bx " + nt.icon,
|
||||
badges: []
|
||||
}
|
||||
function getBlankNoteTypes(command): MenuItem<TreeCommandNames>[] {
|
||||
return NOTE_TYPES.filter((nt) => !nt.reserved).map((nt) => {
|
||||
const menuItem: MenuCommandItem<TreeCommandNames> = {
|
||||
title: nt.title,
|
||||
command,
|
||||
type: nt.type,
|
||||
uiIcon: "bx " + nt.icon,
|
||||
badges: []
|
||||
}
|
||||
|
||||
if (nt.isNew) {
|
||||
menuItem.badges?.push(NEW_BADGE);
|
||||
}
|
||||
if (nt.isNew) {
|
||||
menuItem.badges?.push(NEW_BADGE);
|
||||
}
|
||||
|
||||
if (nt.isBeta) {
|
||||
menuItem.badges?.push(BETA_BADGE);
|
||||
}
|
||||
if (nt.isBeta) {
|
||||
menuItem.badges?.push(BETA_BADGE);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
});
|
||||
return menuItem;
|
||||
});
|
||||
}
|
||||
|
||||
async function getUserTemplates(command?: TreeCommandNames) {
|
||||
@@ -154,15 +152,15 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: MenuItem<TreeCommandNames>[] = [];
|
||||
const items: MenuItem<TreeCommandNames>[] = [
|
||||
SEPARATOR
|
||||
];
|
||||
|
||||
if (title) {
|
||||
items.push({
|
||||
title: title,
|
||||
enabled: false,
|
||||
uiIcon: "bx bx-empty"
|
||||
enabled: false
|
||||
});
|
||||
} else {
|
||||
items.push(SEPARATOR);
|
||||
}
|
||||
|
||||
for (const templateNote of childNotes) {
|
||||
|
||||
@@ -337,11 +337,6 @@ button kbd {
|
||||
--bs-dropdown-link-active-bg: var(--active-item-background-color) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-divider {
|
||||
break-before: avoid;
|
||||
break-after: avoid;
|
||||
}
|
||||
|
||||
body.desktop .dropdown-menu {
|
||||
border: 1px solid var(--dropdown-border-color);
|
||||
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
|
||||
|
||||
@@ -754,7 +754,7 @@
|
||||
"expand_all_children": "展开所有子项",
|
||||
"collapse": "折叠",
|
||||
"expand": "展开",
|
||||
"book_properties": "",
|
||||
"book_properties": "书籍属性",
|
||||
"invalid_view_type": "无效的查看类型 '{{type}}'",
|
||||
"calendar": "日历"
|
||||
},
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
"expand_all_children": "Unternotizen ausklappen",
|
||||
"collapse": "Einklappen",
|
||||
"expand": "Ausklappen",
|
||||
"book_properties": "",
|
||||
"book_properties": "Bucheigenschaften",
|
||||
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
|
||||
"calendar": "Kalender"
|
||||
},
|
||||
|
||||
@@ -758,7 +758,7 @@
|
||||
"expand_all_children": "Expand all children",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"book_properties": "Collection Properties",
|
||||
"book_properties": "Book Properties",
|
||||
"invalid_view_type": "Invalid view type '{{type}}'",
|
||||
"calendar": "Calendar",
|
||||
"table": "Table",
|
||||
@@ -962,7 +962,7 @@
|
||||
"no_attachments": "This note has no attachments."
|
||||
},
|
||||
"book": {
|
||||
"no_children_help": "This collection doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details."
|
||||
"no_children_help": "This note of type Book doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details."
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "Type the content of your code note here..."
|
||||
@@ -1629,8 +1629,7 @@
|
||||
"beta-feature": "Beta",
|
||||
"ai-chat": "AI Chat",
|
||||
"task-list": "Task List",
|
||||
"new-feature": "New",
|
||||
"collections": "Collections"
|
||||
"new-feature": "New"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "Protect the note",
|
||||
@@ -1942,9 +1941,5 @@
|
||||
"table_view": {
|
||||
"new-row": "New row",
|
||||
"new-column": "New column"
|
||||
},
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "Hide weekends",
|
||||
"display-week-numbers": "Display week numbers"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@
|
||||
"expand_all_children": "Ampliar todas las subnotas",
|
||||
"collapse": "Colapsar",
|
||||
"expand": "Expandir",
|
||||
"book_properties": "",
|
||||
"book_properties": "Propiedades del libro",
|
||||
"invalid_view_type": "Tipo de vista inválida '{{type}}'",
|
||||
"calendar": "Calendario"
|
||||
},
|
||||
|
||||
@@ -753,7 +753,7 @@
|
||||
"expand_all_children": "Développer tous les enfants",
|
||||
"collapse": "Réduire",
|
||||
"expand": "Développer",
|
||||
"book_properties": "",
|
||||
"book_properties": "Propriétés du livre",
|
||||
"invalid_view_type": "Type de vue non valide '{{type}}'",
|
||||
"calendar": "Calendrier"
|
||||
},
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
"no_children_help": "Această notiță de tip Carte nu are nicio subnotiță așadar nu este nimic de afișat. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> pentru detalii."
|
||||
},
|
||||
"book_properties": {
|
||||
"book_properties": "",
|
||||
"book_properties": "Proprietăți carte",
|
||||
"collapse": "Minimizează",
|
||||
"collapse_all_notes": "Minimizează toate notițele",
|
||||
"expand": "Expandează",
|
||||
|
||||
@@ -718,7 +718,7 @@
|
||||
"expand_all_children": "展開所有子項",
|
||||
"collapse": "折疊",
|
||||
"expand": "展開",
|
||||
"book_properties": "",
|
||||
"book_properties": "書籍屬性",
|
||||
"invalid_view_type": "無效的查看類型 '{{type}}'"
|
||||
},
|
||||
"edited_notes": {
|
||||
|
||||
@@ -3,8 +3,6 @@ import attributeService from "../../services/attributes.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import { bookPropertiesConfig, BookProperty } from "./book_properties_config.js";
|
||||
import attributes from "../../services/attributes.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="book-properties-widget">
|
||||
@@ -17,19 +15,6 @@ const TPL = /*html*/`
|
||||
.book-properties-widget > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.book-properties-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.book-properties-container > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.book-properties-container input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style="display: flex; align-items: baseline">
|
||||
@@ -44,16 +29,30 @@ const TPL = /*html*/`
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="book-properties-container">
|
||||
</div>
|
||||
<button type="button"
|
||||
class="collapse-all-button btn btn-sm"
|
||||
title="${t("book_properties.collapse_all_notes")}">
|
||||
|
||||
<span class="bx bx-layer-minus"></span>
|
||||
|
||||
${t("book_properties.collapse")}
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="expand-children-button btn btn-sm"
|
||||
title="${t("book_properties.expand_all_children")}">
|
||||
<span class="bx bx-move-vertical"></span>
|
||||
|
||||
${t("book_properties.expand")}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $viewTypeSelect!: JQuery<HTMLElement>;
|
||||
private $propertiesContainer!: JQuery<HTMLElement>;
|
||||
private labelsToWatch: string[] = [];
|
||||
private $expandChildrenButton!: JQuery<HTMLElement>;
|
||||
private $collapseAllButton!: JQuery<HTMLElement>;
|
||||
|
||||
get name() {
|
||||
return "bookProperties";
|
||||
@@ -82,7 +81,32 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
this.$viewTypeSelect = this.$widget.find(".view-type-select");
|
||||
this.$viewTypeSelect.on("change", () => this.toggleViewType(String(this.$viewTypeSelect.val())));
|
||||
|
||||
this.$propertiesContainer = this.$widget.find(".book-properties-container");
|
||||
this.$expandChildrenButton = this.$widget.find(".expand-children-button");
|
||||
this.$expandChildrenButton.on("click", async () => {
|
||||
if (!this.noteId || !this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.note?.isLabelTruthy("expanded")) {
|
||||
await attributeService.addLabel(this.noteId, "expanded");
|
||||
}
|
||||
|
||||
this.triggerCommand("refreshNoteList", { noteId: this.noteId });
|
||||
});
|
||||
|
||||
this.$collapseAllButton = this.$widget.find(".collapse-all-button");
|
||||
this.$collapseAllButton.on("click", async () => {
|
||||
if (!this.noteId || !this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of this.note.getOwnedLabels("expanded")) {
|
||||
await attributeService.removeAttributeById(this.noteId, expandedAttr.attributeId);
|
||||
}
|
||||
|
||||
this.triggerCommand("refreshNoteList", { noteId: this.noteId });
|
||||
});
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
@@ -94,15 +118,8 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$viewTypeSelect.val(viewType);
|
||||
|
||||
this.$propertiesContainer.empty();
|
||||
|
||||
const bookPropertiesData = bookPropertiesConfig[viewType];
|
||||
if (bookPropertiesData) {
|
||||
for (const property of bookPropertiesData.properties) {
|
||||
this.$propertiesContainer.append(this.renderBookProperty(property));
|
||||
this.labelsToWatch.push(property.bindToLabel);
|
||||
}
|
||||
}
|
||||
this.$expandChildrenButton.toggle(viewType === "list");
|
||||
this.$collapseAllButton.toggle(viewType === "list");
|
||||
}
|
||||
|
||||
async toggleViewType(type: string) {
|
||||
@@ -118,60 +135,8 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows().find((attr) =>
|
||||
attr.noteId === this.noteId
|
||||
&& (attr.name === "viewType" || this.labelsToWatch.includes(attr.name ?? "")))) {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name === "viewType")) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
renderBookProperty(property: BookProperty) {
|
||||
const $container = $("<div>");
|
||||
const note = this.note;
|
||||
if (!note) {
|
||||
return $container;
|
||||
}
|
||||
switch (property.type) {
|
||||
case "checkbox":
|
||||
const $label = $("<label>").text(property.label);
|
||||
const $checkbox = $("<input>", {
|
||||
type: "checkbox",
|
||||
class: "form-check-input",
|
||||
});
|
||||
$checkbox.on("change", () => {
|
||||
if ($checkbox.prop("checked")) {
|
||||
attributes.setLabel(note.noteId, property.bindToLabel);
|
||||
} else {
|
||||
attributes.removeOwnedLabelByName(note, property.bindToLabel);
|
||||
}
|
||||
});
|
||||
$checkbox.prop("checked", note.hasOwnedLabel(property.bindToLabel));
|
||||
$label.prepend($checkbox);
|
||||
$container.append($label);
|
||||
break;
|
||||
case "button":
|
||||
const $button = $("<button>", {
|
||||
type: "button",
|
||||
class: "btn btn-sm"
|
||||
}).text(property.label);
|
||||
if (property.title) {
|
||||
$button.attr("title", property.title);
|
||||
}
|
||||
if (property.icon) {
|
||||
$button.prepend($("<span>", { class: property.icon }));
|
||||
}
|
||||
$button.on("click", () => {
|
||||
property.onClick({
|
||||
note,
|
||||
triggerCommand: this.triggerCommand.bind(this)
|
||||
});
|
||||
});
|
||||
$container.append($button);
|
||||
break;
|
||||
}
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { t } from "i18next";
|
||||
import FNote from "../../entities/fnote";
|
||||
import attributes from "../../services/attributes";
|
||||
import { ViewTypeOptions } from "../../services/note_list_renderer"
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
|
||||
export type BookProperty = CheckBoxProperty | ButtonProperty;
|
||||
|
||||
interface BookConfig {
|
||||
properties: BookProperty[];
|
||||
}
|
||||
|
||||
interface CheckBoxProperty {
|
||||
type: "checkbox",
|
||||
label: string;
|
||||
bindToLabel: string
|
||||
}
|
||||
|
||||
interface ButtonProperty {
|
||||
type: "button",
|
||||
label: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
onClick: (context: BookContext) => void;
|
||||
}
|
||||
|
||||
interface BookContext {
|
||||
note: FNote;
|
||||
triggerCommand: NoteContextAwareWidget["triggerCommand"];
|
||||
}
|
||||
|
||||
export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
grid: {
|
||||
properties: []
|
||||
},
|
||||
list: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties.collapse"),
|
||||
title: t("book_properties.collapse_all_notes"),
|
||||
type: "button",
|
||||
icon: "bx bx-layer-minus",
|
||||
async onClick({ note, triggerCommand }) {
|
||||
const { noteId } = note;
|
||||
|
||||
// owned is important - we shouldn't remove inherited expanded labels
|
||||
for (const expandedAttr of note.getOwnedLabels("expanded")) {
|
||||
await attributes.removeAttributeById(noteId, expandedAttr.attributeId);
|
||||
}
|
||||
|
||||
triggerCommand("refreshNoteList", { noteId: noteId });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("book_properties.expand"),
|
||||
title: t("book_properties.expand_all_children"),
|
||||
type: "button",
|
||||
icon: "bx bx-move-vertical",
|
||||
async onClick({ note, triggerCommand }) {
|
||||
const { noteId } = note;
|
||||
if (!note.isLabelTruthy("expanded")) {
|
||||
await attributes.addLabel(noteId, "expanded");
|
||||
}
|
||||
|
||||
triggerCommand("refreshNoteList", { noteId });
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
calendar: {
|
||||
properties: [
|
||||
{
|
||||
label: t("book_properties_config.hide-weekends"),
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:hideWeekends"
|
||||
},
|
||||
{
|
||||
label: t("book_properties_config.display-week-numbers"),
|
||||
type: "checkbox",
|
||||
bindToLabel: "calendar:weekNumbers"
|
||||
}
|
||||
]
|
||||
},
|
||||
geoMap: {
|
||||
properties: []
|
||||
},
|
||||
table: {
|
||||
properties: []
|
||||
}
|
||||
};
|
||||
@@ -37,10 +37,7 @@ const config: ForgeConfig = {
|
||||
executableName: EXECUTABLE_NAME,
|
||||
name: PRODUCT_NAME,
|
||||
overwrite: true,
|
||||
asar: {
|
||||
unpack: "node_modules/swagger-ui-dist/**",
|
||||
unpackDir: "assets"
|
||||
},
|
||||
asar: true,
|
||||
icon: path.join(APP_ICON_PATH, "icon"),
|
||||
...macosSignConfiguration,
|
||||
windowsSign: windowsSignConfiguration,
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.3.2",
|
||||
"i18next": "25.3.1",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "5.0.0",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"reset-zoom-level": "Reset zoom level",
|
||||
"copy-without-formatting": "Copy selected text without formatting",
|
||||
"force-save-revision": "Force creating / saving new note revision of the active note",
|
||||
"toggle-book-properties": "Toggle Collection Properties",
|
||||
"toggle-book-properties": "Toggle Book Properties",
|
||||
"toggle-classic-editor-toolbar": "Toggle the Formatting tab for the editor with fixed toolbar",
|
||||
"export-as-pdf": "Export the current note as a PDF",
|
||||
"toggle-zen-mode": "Enables/disables the zen mode (minimal UI for more focused editing)"
|
||||
@@ -303,20 +303,5 @@
|
||||
"subpages": "Subpages:",
|
||||
"on-this-page": "On This Page",
|
||||
"expand": "Expand"
|
||||
},
|
||||
"hidden_subtree_templates": {
|
||||
"text-snippet": "Text Snippet",
|
||||
"description": "Description",
|
||||
"list-view": "List View",
|
||||
"grid-view": "Grid View",
|
||||
"calendar": "Calendar",
|
||||
"table": "Table",
|
||||
"geo-map": "Geo Map",
|
||||
"start-date": "Start Date",
|
||||
"end-date": "End Date",
|
||||
"start-time": "Start Time",
|
||||
"end-time": "End Time",
|
||||
"geolocation": "Geolocation",
|
||||
"built-in-templates": "Built-in templates"
|
||||
}
|
||||
}
|
||||
|
||||
148
apps/server/src/routes/api_docs.spec.ts
Normal file
148
apps/server/src/routes/api_docs.spec.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import type { Application } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { RESOURCE_DIR } from "../services/resource_dir.js";
|
||||
|
||||
let app: Application;
|
||||
|
||||
describe("API Documentation Routes", () => {
|
||||
beforeAll(async () => {
|
||||
const buildApp = (await import("../app.js")).default;
|
||||
app = await buildApp();
|
||||
});
|
||||
|
||||
describe("ETAPI Documentation", () => {
|
||||
it("should serve ETAPI Swagger UI at /etapi/docs/", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/text\/html/);
|
||||
expect(response.text).toContain("TriliumNext ETAPI Documentation");
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
});
|
||||
|
||||
it("should have OpenAPI spec accessible through Swagger UI", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
expect(response.text).toContain("TriliumNext ETAPI Documentation");
|
||||
});
|
||||
|
||||
it("should serve ETAPI static assets", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/swagger-ui-bundle.js")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/javascript/);
|
||||
});
|
||||
|
||||
it("should load ETAPI OpenAPI spec from correct resource path", () => {
|
||||
const etapiSpecPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml");
|
||||
expect(fs.existsSync(etapiSpecPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Internal API Documentation", () => {
|
||||
it("should serve Internal API Swagger UI at /api/docs/", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/api/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/text\/html/);
|
||||
expect(response.text).toContain("TriliumNext Internal API Documentation");
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
});
|
||||
|
||||
it("should have OpenAPI spec accessible through Swagger UI", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/api/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
expect(response.text).toContain("TriliumNext Internal API Documentation");
|
||||
});
|
||||
|
||||
it("should serve Internal API static assets", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/api/docs/swagger-ui-bundle.js")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/javascript/);
|
||||
});
|
||||
|
||||
it("should load Internal API OpenAPI spec from correct resource path", () => {
|
||||
const apiSpecPath = path.join(RESOURCE_DIR, "openapi.json");
|
||||
expect(fs.existsSync(apiSpecPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Resource Directory Resolution", () => {
|
||||
it("should resolve RESOURCE_DIR to a valid directory", () => {
|
||||
expect(fs.existsSync(RESOURCE_DIR)).toBe(true);
|
||||
expect(fs.statSync(RESOURCE_DIR).isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
it("should find assets directory in RESOURCE_DIR", () => {
|
||||
const assetsPath = path.join(RESOURCE_DIR, "assets");
|
||||
// The assets directory should exist at the resource root, not inside another assets folder
|
||||
expect(fs.existsSync(RESOURCE_DIR)).toBe(true);
|
||||
});
|
||||
|
||||
it("should have required OpenAPI files in RESOURCE_DIR", () => {
|
||||
const etapiPath = path.join(RESOURCE_DIR, "etapi.openapi.yaml");
|
||||
const openApiPath = path.join(RESOURCE_DIR, "openapi.json");
|
||||
|
||||
expect(fs.existsSync(etapiPath)).toBe(true);
|
||||
expect(fs.existsSync(openApiPath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle missing OpenAPI files gracefully", async () => {
|
||||
// Mock fs.readFileSync to throw an error
|
||||
const originalReadFileSync = fs.readFileSync;
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation((path, options) => {
|
||||
if (typeof path === "string" && path.includes("etapi.openapi.yaml")) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
return originalReadFileSync(path, options);
|
||||
});
|
||||
|
||||
try {
|
||||
await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(500);
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
}
|
||||
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should handle invalid OpenAPI files gracefully", async () => {
|
||||
// Mock fs.readFileSync to return invalid YAML
|
||||
const originalReadFileSync = fs.readFileSync;
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation((path, options) => {
|
||||
if (typeof path === "string" && path.includes("etapi.openapi.yaml")) {
|
||||
return "invalid: yaml: content: [" as any;
|
||||
}
|
||||
return originalReadFileSync(path, options);
|
||||
});
|
||||
|
||||
try {
|
||||
await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(500);
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
}
|
||||
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,102 +3,19 @@ import swaggerUi from "swagger-ui-express";
|
||||
import { join } from "path";
|
||||
import yaml from "js-yaml";
|
||||
import type { JsonObject } from "swagger-ui-express";
|
||||
import { readFileSync, existsSync, readdirSync } from "fs";
|
||||
import { readFileSync } from "fs";
|
||||
import { RESOURCE_DIR } from "../services/resource_dir";
|
||||
import log from "../services/log";
|
||||
|
||||
// Monkey patch fs.lstat to debug which files are causing the issue
|
||||
const originalFs = require('fs');
|
||||
const originalLstat = originalFs.lstat;
|
||||
originalFs.lstat = function(path: string, callback: any) {
|
||||
log.info(`[FS DEBUG] lstat called on: ${path}`);
|
||||
return originalLstat.call(this, path, (err: any, stats: any) => {
|
||||
if (err) {
|
||||
log.error(`[FS DEBUG] lstat error on ${path}: ${err.message}`);
|
||||
}
|
||||
callback(err, stats);
|
||||
});
|
||||
};
|
||||
|
||||
export default function register(app: Application) {
|
||||
log.info(`[DEBUG] Starting API docs registration`);
|
||||
log.info(`[DEBUG] RESOURCE_DIR: ${RESOURCE_DIR}`);
|
||||
|
||||
// Clean trailing slashes from RESOURCE_DIR to prevent path resolution issues in packaged Electron apps
|
||||
const cleanResourceDir = RESOURCE_DIR.replace(/[\\\/]+$/, '');
|
||||
log.info(`[DEBUG] cleanResourceDir: ${cleanResourceDir}`);
|
||||
|
||||
// Check what's in the resource directory
|
||||
try {
|
||||
if (existsSync(cleanResourceDir)) {
|
||||
const contents = readdirSync(cleanResourceDir);
|
||||
log.info(`[DEBUG] Contents of ${cleanResourceDir}: ${contents.join(', ')}`);
|
||||
} else {
|
||||
log.info(`[DEBUG] Resource directory doesn't exist: ${cleanResourceDir}`);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(`[DEBUG] Error reading resource directory: ${e}`);
|
||||
}
|
||||
|
||||
// In packaged Electron apps, check if we need to read from the unpacked directory
|
||||
let resourceDir = cleanResourceDir;
|
||||
if (resourceDir.includes('app.asar')) {
|
||||
log.info(`[DEBUG] Detected ASAR packaging`);
|
||||
const unpackedDir = cleanResourceDir.replace('app.asar', 'app.asar.unpacked');
|
||||
log.info(`[DEBUG] Checking unpacked dir: ${unpackedDir}`);
|
||||
|
||||
// Check what's in the unpacked directory
|
||||
try {
|
||||
if (existsSync(unpackedDir)) {
|
||||
const unpackedContents = readdirSync(unpackedDir);
|
||||
log.info(`[DEBUG] Contents of unpacked dir: ${unpackedContents.join(', ')}`);
|
||||
if (existsSync(join(unpackedDir, "etapi.openapi.yaml"))) {
|
||||
resourceDir = unpackedDir;
|
||||
log.info(`[DEBUG] Using unpacked directory: ${resourceDir}`);
|
||||
}
|
||||
} else {
|
||||
log.info(`[DEBUG] Unpacked directory doesn't exist: ${unpackedDir}`);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(`[DEBUG] Error checking unpacked directory: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`[DEBUG] Final resourceDir: ${resourceDir}`);
|
||||
log.info(`[DEBUG] About to load OpenAPI specs...`);
|
||||
|
||||
const etapiDocument = yaml.load(readFileSync(join(resourceDir, "etapi.openapi.yaml"), "utf8")) as JsonObject;
|
||||
const apiDocument = JSON.parse(readFileSync(join(resourceDir, "openapi.json"), "utf-8"));
|
||||
|
||||
log.info(`[DEBUG] Successfully loaded OpenAPI documents`);
|
||||
log.info(`[DEBUG] About to register swagger-ui endpoints...`);
|
||||
|
||||
// Check swagger-ui-dist location
|
||||
try {
|
||||
const swaggerPath = require.resolve('swagger-ui-dist/package.json');
|
||||
log.info(`[DEBUG] swagger-ui-dist package.json found at: ${swaggerPath}`);
|
||||
const swaggerDistPath = require.resolve('swagger-ui-dist');
|
||||
log.info(`[DEBUG] swagger-ui-dist main module at: ${swaggerDistPath}`);
|
||||
} catch (e) {
|
||||
log.error(`[DEBUG] Error finding swagger-ui-dist: ${e}`);
|
||||
}
|
||||
const etapiDocument = yaml.load(readFileSync(join(cleanResourceDir, "etapi.openapi.yaml"), "utf8")) as JsonObject;
|
||||
const apiDocument = JSON.parse(readFileSync(join(cleanResourceDir, "openapi.json"), "utf-8"));
|
||||
|
||||
app.use(
|
||||
"/etapi/docs/",
|
||||
(req, res, next) => {
|
||||
log.info(`[DEBUG] Request to /etapi/docs/: ${req.method} ${req.url}`);
|
||||
// Temporarily disable ASAR for swagger-ui file access
|
||||
const originalNoAsar = process.noAsar;
|
||||
process.noAsar = true;
|
||||
|
||||
// Restore ASAR setting after response
|
||||
res.on('finish', () => {
|
||||
process.noAsar = originalNoAsar;
|
||||
});
|
||||
|
||||
next();
|
||||
},
|
||||
swaggerUi.serve,
|
||||
swaggerUi.serveFiles(etapiDocument),
|
||||
swaggerUi.setup(etapiDocument, {
|
||||
explorer: true,
|
||||
customSiteTitle: "TriliumNext ETAPI Documentation"
|
||||
@@ -107,19 +24,7 @@ export default function register(app: Application) {
|
||||
|
||||
app.use(
|
||||
"/api/docs/",
|
||||
(req, res, next) => {
|
||||
// Temporarily disable ASAR for swagger-ui file access
|
||||
const originalNoAsar = process.noAsar;
|
||||
process.noAsar = true;
|
||||
|
||||
// Restore ASAR setting after response
|
||||
res.on('finish', () => {
|
||||
process.noAsar = originalNoAsar;
|
||||
});
|
||||
|
||||
next();
|
||||
},
|
||||
swaggerUi.serve,
|
||||
swaggerUi.serveFiles(apiDocument),
|
||||
swaggerUi.setup(apiDocument, {
|
||||
explorer: true,
|
||||
customSiteTitle: "TriliumNext Internal API Documentation"
|
||||
|
||||
139
apps/server/src/services/api_reference_help.spec.ts
Normal file
139
apps/server/src/services/api_reference_help.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import type { Application } from "express";
|
||||
import { note } from "../test/becca_mocking.js";
|
||||
import BNote from "../becca/entities/bnote.js";
|
||||
|
||||
let app: Application;
|
||||
|
||||
describe("API Reference Help Note", () => {
|
||||
beforeAll(async () => {
|
||||
const buildApp = (await import("../app.js")).default;
|
||||
app = await buildApp();
|
||||
});
|
||||
|
||||
describe("Help Note Structure", () => {
|
||||
it("should have correct help note metadata in the system", () => {
|
||||
// Test that the help note IDs are defined in the system
|
||||
expect("_help_9qPsTWBorUhQ").toBe("_help_9qPsTWBorUhQ");
|
||||
expect("_help_z8O2VG4ZZJD7").toBe("_help_z8O2VG4ZZJD7");
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebView Source URLs", () => {
|
||||
it("should serve content at ETAPI docs URL", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/text\/html/);
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
});
|
||||
|
||||
it("should serve content at Internal API docs URL", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/api/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/text\/html/);
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
});
|
||||
|
||||
it("should handle trailing slash in ETAPI docs URL", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/text\/html/);
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
});
|
||||
|
||||
it("should handle trailing slash in Internal API docs URL", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/api/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers["content-type"]).toMatch(/text\/html/);
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Help Note Integration", () => {
|
||||
it("should be accessible via help note ID in the application", async () => {
|
||||
// Test that the help note endpoint would work
|
||||
// Note: This would typically be handled by the client-side application
|
||||
// but we can test that the webViewSrc URL is accessible
|
||||
|
||||
const etapiResponse = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(etapiResponse.text).toContain("TriliumNext ETAPI Documentation");
|
||||
});
|
||||
|
||||
it("should not return 'Invalid package' error for ETAPI docs", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.text).not.toContain("Invalid package");
|
||||
expect(response.text).not.toContain("C:\\\\Users\\\\perf3ct\\\\AppData\\\\Local\\\\trilium\\\\app-0.96.0\\\\resources\\\\app.asar");
|
||||
});
|
||||
|
||||
it("should not return 'Invalid package' error for Internal API docs", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/api/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.text).not.toContain("Invalid package");
|
||||
expect(response.text).not.toContain("C:\\\\Users\\\\perf3ct\\\\AppData\\\\Local\\\\trilium\\\\app-0.96.0\\\\resources\\\\app.asar");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Swagger UI Content Validation", () => {
|
||||
it("should serve valid Swagger UI page with expected elements", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
// Check for essential Swagger UI elements
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
expect(response.text).toContain("TriliumNext ETAPI Documentation");
|
||||
expect(response.text).toMatch(/swagger-ui.*css/);
|
||||
expect(response.text).toMatch(/swagger-ui.*js/);
|
||||
});
|
||||
|
||||
it("should serve valid Swagger UI with OpenAPI content", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
expect(response.text).toContain("TriliumNext ETAPI Documentation");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Client-Side WebView Integration", () => {
|
||||
it("should serve content that can be loaded in a webview", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
// Check that the response is proper HTML that can be loaded in a webview
|
||||
expect(response.text).toMatch(/<!DOCTYPE html>/i);
|
||||
expect(response.text).toMatch(/<html/i);
|
||||
expect(response.text).toMatch(/<head>/i);
|
||||
expect(response.text).toMatch(/<body>/i);
|
||||
});
|
||||
|
||||
it("should have appropriate headers for webview loading", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/docs/")
|
||||
.expect(200);
|
||||
|
||||
// Check that the response is successful and has content
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toContain("swagger-ui");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,15 @@
|
||||
import { HiddenSubtreeItem } from "@triliumnext/commons";
|
||||
import { t } from "i18next";
|
||||
|
||||
export default function buildHiddenSubtreeTemplates() {
|
||||
const templates: HiddenSubtreeItem = {
|
||||
id: "_templates",
|
||||
title: t("hidden_subtree_templates.built-in-templates"),
|
||||
title: "Built-in templates",
|
||||
type: "book",
|
||||
children: [
|
||||
{
|
||||
id: "_template_text_snippet",
|
||||
type: "text",
|
||||
title: t("hidden_subtree_templates.text-snippet"),
|
||||
title: "Text Snippet",
|
||||
icon: "bx-align-left",
|
||||
attributes: [
|
||||
{
|
||||
@@ -24,105 +23,14 @@ export default function buildHiddenSubtreeTemplates() {
|
||||
{
|
||||
name: "label:textSnippetDescription",
|
||||
type: "label",
|
||||
value: `promoted,alias=${t("hidden_subtree_templates.description")},single,text`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_list_view",
|
||||
type: "book",
|
||||
title: t("hidden_subtree_templates.list-view"),
|
||||
icon: "bx bx-list-ul",
|
||||
attributes: [
|
||||
{
|
||||
name: "template",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "collection",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "viewType",
|
||||
type: "label",
|
||||
value: "list"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_grid_view",
|
||||
type: "book",
|
||||
title: t("hidden_subtree_templates.grid-view"),
|
||||
icon: "bx bxs-grid",
|
||||
attributes: [
|
||||
{
|
||||
name: "template",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "collection",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "viewType",
|
||||
type: "label",
|
||||
value: "grid"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_calendar",
|
||||
type: "book",
|
||||
title: t("hidden_subtree_templates.calendar"),
|
||||
icon: "bx bx-calendar",
|
||||
attributes: [
|
||||
{
|
||||
name: "template",
|
||||
type: "label",
|
||||
},
|
||||
{
|
||||
name: "collection",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "viewType",
|
||||
type: "label",
|
||||
value: "calendar"
|
||||
},
|
||||
{
|
||||
name: "hidePromotedAttributes",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "label:startDate",
|
||||
type: "label",
|
||||
value: `promoted,alias=${t("hidden_subtree_templates.start-date")},single,date`,
|
||||
isInheritable: true
|
||||
},
|
||||
{
|
||||
name: "label:endDate",
|
||||
type: "label",
|
||||
value: `promoted,alias=${t("hidden_subtree_templates.end-date")},single,date`,
|
||||
isInheritable: true
|
||||
},
|
||||
{
|
||||
name: "label:startTime",
|
||||
type: "label",
|
||||
value: `promoted,alias=${t("hidden_subtree_templates.start-time")},single,time`,
|
||||
isInheritable: true
|
||||
},
|
||||
{
|
||||
name: "label:endTime",
|
||||
type: "label",
|
||||
value: `promoted,alias=${t("hidden_subtree_templates.end-time")},single,time`,
|
||||
isInheritable: true
|
||||
value: "promoted,alias=Description,single,text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_table",
|
||||
type: "book",
|
||||
title: t("hidden_subtree_templates.table"),
|
||||
title: "Table",
|
||||
icon: "bx bx-table",
|
||||
attributes: [
|
||||
{
|
||||
@@ -143,7 +51,7 @@ export default function buildHiddenSubtreeTemplates() {
|
||||
{
|
||||
id: "_template_geo_map",
|
||||
type: "book",
|
||||
title: t("hidden_subtree_templates.geo-map"),
|
||||
title: "Geo Map",
|
||||
icon: "bx bx-map-alt",
|
||||
attributes: [
|
||||
{
|
||||
@@ -166,7 +74,7 @@ export default function buildHiddenSubtreeTemplates() {
|
||||
{
|
||||
name: "label:geolocation",
|
||||
type: "label",
|
||||
value: `promoted,alias=${t("hidden_subtree_templates.geolocation")},single,text`,
|
||||
value: "promoted,alias=Geolocation,single,text",
|
||||
isInheritable: true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import utils from "./utils.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
|
||||
|
||||
@@ -474,7 +476,110 @@ describe("#envToBoolean", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.todo("#getResourceDir", () => {});
|
||||
describe("#getResourceDir", () => {
|
||||
let originalEnv: typeof process.env;
|
||||
let originalPlatform: typeof process.platform;
|
||||
let originalVersions: typeof process.versions;
|
||||
let originalArgv: typeof process.argv;
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original values
|
||||
originalEnv = { ...process.env };
|
||||
originalPlatform = process.platform;
|
||||
originalVersions = { ...process.versions };
|
||||
originalArgv = [...process.argv];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original values
|
||||
process.env = originalEnv;
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
Object.defineProperty(process, 'versions', { value: originalVersions });
|
||||
process.argv = originalArgv;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return TRILIUM_RESOURCE_DIR environment variable when set", () => {
|
||||
const testPath = "/custom/resource/dir";
|
||||
process.env.TRILIUM_RESOURCE_DIR = testPath;
|
||||
|
||||
const result = utils.getResourceDir();
|
||||
expect(result).toBe(testPath);
|
||||
});
|
||||
|
||||
it("should dynamically find assets directory in Electron production mode", () => {
|
||||
// Mock Electron production environment
|
||||
delete process.env.TRILIUM_RESOURCE_DIR;
|
||||
Object.defineProperty(process, 'versions', {
|
||||
value: { ...originalVersions, electron: '37.2.0' }
|
||||
});
|
||||
delete process.env.TRILIUM_ENV;
|
||||
|
||||
// Mock fs.existsSync to simulate finding assets directory
|
||||
const mockExistsSync = vi.spyOn(fs, 'existsSync');
|
||||
mockExistsSync.mockImplementation((pathToCheck: string) => {
|
||||
// Simulate finding assets directory at the app root level
|
||||
return pathToCheck.includes("assets") &&
|
||||
pathToCheck === path.join(path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))), "assets");
|
||||
});
|
||||
|
||||
const result = utils.getResourceDir();
|
||||
expect(result).toBe(path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))));
|
||||
expect(mockExistsSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should fall back to hardcoded path when assets directory not found dynamically", () => {
|
||||
// Mock Electron production environment
|
||||
delete process.env.TRILIUM_RESOURCE_DIR;
|
||||
Object.defineProperty(process, 'versions', {
|
||||
value: { ...originalVersions, electron: '37.2.0' }
|
||||
});
|
||||
delete process.env.TRILIUM_ENV;
|
||||
|
||||
// Mock fs.existsSync to always return false (assets not found)
|
||||
const mockExistsSync = vi.spyOn(fs, 'existsSync');
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = utils.getResourceDir();
|
||||
expect(result).toBe(path.join(__dirname, "../../../.."));
|
||||
expect(mockExistsSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return process.argv[1] directory in non-Electron production mode", () => {
|
||||
delete process.env.TRILIUM_RESOURCE_DIR;
|
||||
Object.defineProperty(process, 'versions', {
|
||||
value: { ...originalVersions, electron: undefined }
|
||||
});
|
||||
delete process.env.TRILIUM_ENV;
|
||||
|
||||
process.argv[1] = "/app/server/main.js";
|
||||
|
||||
const result = utils.getResourceDir();
|
||||
expect(result).toBe(path.dirname(process.argv[1]));
|
||||
});
|
||||
|
||||
it("should return parent directory in development mode", () => {
|
||||
delete process.env.TRILIUM_RESOURCE_DIR;
|
||||
process.env.TRILIUM_ENV = "dev";
|
||||
|
||||
const result = utils.getResourceDir();
|
||||
expect(result).toBe(path.join(__dirname, ".."));
|
||||
});
|
||||
|
||||
it("should handle edge case when reaching filesystem root", () => {
|
||||
delete process.env.TRILIUM_RESOURCE_DIR;
|
||||
Object.defineProperty(process, 'versions', {
|
||||
value: { ...originalVersions, electron: '37.2.0' }
|
||||
});
|
||||
delete process.env.TRILIUM_ENV;
|
||||
|
||||
const mockExistsSync = vi.spyOn(fs, 'existsSync');
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = utils.getResourceDir();
|
||||
expect(result).toBe(path.join(__dirname, "../../../.."));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#isElectron", () => {
|
||||
it("should export a boolean", () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import escape from "escape-html";
|
||||
import sanitize from "sanitize-filename";
|
||||
import mimeTypes from "mime-types";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import type NoteMeta from "./meta/note_meta.js";
|
||||
import log from "./log.js";
|
||||
import { t } from "i18next";
|
||||
@@ -292,7 +293,33 @@ export function getResourceDir() {
|
||||
return process.env.TRILIUM_RESOURCE_DIR;
|
||||
}
|
||||
|
||||
if (isElectron && !isDev) return __dirname;
|
||||
if (isElectron && !isDev) {
|
||||
// Dynamically find the correct resource directory by traversing upward
|
||||
// until we find the assets directory or reach the root
|
||||
let currentPath = __dirname;
|
||||
let maxDepth = 10; // Safety limit to prevent infinite loops
|
||||
|
||||
while (maxDepth > 0) {
|
||||
const assetsPath = path.join(currentPath, "assets");
|
||||
if (fs.existsSync(assetsPath)) {
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
const parentPath = path.dirname(currentPath);
|
||||
if (parentPath === currentPath) {
|
||||
// Reached root directory
|
||||
break;
|
||||
}
|
||||
|
||||
currentPath = parentPath;
|
||||
maxDepth--;
|
||||
}
|
||||
|
||||
// Fallback to the old hardcoded path if dynamic search fails
|
||||
log.info("Could not dynamically find assets directory, falling back to hardcoded path");
|
||||
return path.join(__dirname, "../../../..");
|
||||
}
|
||||
|
||||
if (!isDev) {
|
||||
return path.dirname(process.argv[1]);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"url": "https://github.com/TriliumNext/Notes/issues"
|
||||
},
|
||||
"homepage": "https://github.com/TriliumNext/Notes#readme",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
|
||||
|
||||
433
pnpm-lock.yaml
generated
433
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user