mirror of
https://github.com/zadam/trilium.git
synced 2025-11-10 16:39:02 +01:00
Merge branch 'main' of https://github.com/TriliumNext/Trilium into feat/ui-improvements
This commit is contained in:
commit
728f574eac
@ -1,3 +1,5 @@
|
|||||||
|
@import "boxicons/css/boxicons.min.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--print-font-size: 11pt;
|
--print-font-size: 11pt;
|
||||||
--ck-content-color-image-caption-background: transparent !important;
|
--ck-content-color-image-caption-background: transparent !important;
|
||||||
|
|||||||
@ -2040,6 +2040,9 @@
|
|||||||
"start-presentation": "Start presentation",
|
"start-presentation": "Start presentation",
|
||||||
"slide-overview": "Toggle an overview of the slides"
|
"slide-overview": "Toggle an overview of the slides"
|
||||||
},
|
},
|
||||||
|
"calendar_view": {
|
||||||
|
"delete_note": "Delete note..."
|
||||||
|
},
|
||||||
"command_palette": {
|
"command_palette": {
|
||||||
"tree-action-name": "Tree: {{name}}",
|
"tree-action-name": "Tree: {{name}}",
|
||||||
"export_note_title": "Export Note",
|
"export_note_title": "Export Note",
|
||||||
|
|||||||
28
apps/client/src/widgets/collections/calendar/context_menu.ts
Normal file
28
apps/client/src/widgets/collections/calendar/context_menu.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import FNote from "../../../entities/fnote";
|
||||||
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
|
import branches from "../../../services/branches";
|
||||||
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
|
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
contextMenu.show({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
items: [
|
||||||
|
...link_context_menu.getItems(),
|
||||||
|
{ kind: "separator" },
|
||||||
|
{
|
||||||
|
title: t("calendar_view.delete_note"),
|
||||||
|
uiIcon: "bx bx-trash",
|
||||||
|
handler: async () => {
|
||||||
|
const branchId = parentNote.childToBranch[noteId];
|
||||||
|
await branches.deleteNotes([ branchId ], false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import Button, { ButtonGroup } from "../../react/Button";
|
|||||||
import ActionButton from "../../react/ActionButton";
|
import ActionButton from "../../react/ActionButton";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||||
|
import { openCalendarContextMenu } from "./context_menu";
|
||||||
|
|
||||||
interface CalendarViewData {
|
interface CalendarViewData {
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
|||||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
const { eventDidMount } = useEventDisplayCustomization();
|
const { eventDidMount } = useEventDisplayCustomization(note);
|
||||||
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
||||||
|
|
||||||
// React to changes.
|
// React to changes.
|
||||||
@ -253,7 +254,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEventDisplayCustomization() {
|
function useEventDisplayCustomization(parentNote: FNote) {
|
||||||
const eventDidMount = useCallback((e: EventMountArg) => {
|
const eventDidMount = useCallback((e: EventMountArg) => {
|
||||||
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
||||||
|
|
||||||
@ -302,6 +303,11 @@ function useEventDisplayCustomization() {
|
|||||||
}
|
}
|
||||||
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
|
||||||
|
const noteId = e.event.extendedProps.noteId;
|
||||||
|
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
return { eventDidMount };
|
return { eventDidMount };
|
||||||
}
|
}
|
||||||
|
|||||||
502
apps/server-e2e/src/exact_search.spec.ts
Normal file
502
apps/server-e2e/src/exact_search.spec.ts
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
const BASE_URL = "http://127.0.0.1:8082";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2E tests for exact search functionality using the leading "=" operator.
|
||||||
|
*
|
||||||
|
* These tests validate the GitHub issue:
|
||||||
|
* - Searching for "pagio" returns many false positives (e.g., "page", "pages")
|
||||||
|
* - Searching for "=pagio" should return ONLY exact matches for "pagio"
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe("Exact Search with Leading = Operator", () => {
|
||||||
|
let csrfToken: string;
|
||||||
|
let createdNoteIds: string[] = [];
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Get CSRF token
|
||||||
|
csrfToken = await page.evaluate(() => {
|
||||||
|
return (window as any).glob.csrfToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(csrfToken).toBeTruthy();
|
||||||
|
|
||||||
|
// Create test notes with specific content patterns
|
||||||
|
// Note 1: Contains exactly "pagio" in title
|
||||||
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with pagio",
|
||||||
|
content: "This note contains the word pagio in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note1.ok()).toBeTruthy();
|
||||||
|
const note1Data = await note1.json();
|
||||||
|
createdNoteIds.push(note1Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 2: Contains "page" (not exact match)
|
||||||
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with page",
|
||||||
|
content: "This note contains the word page in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note2.ok()).toBeTruthy();
|
||||||
|
const note2Data = await note2.json();
|
||||||
|
createdNoteIds.push(note2Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 3: Contains "pages" (plural, not exact match)
|
||||||
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with pages",
|
||||||
|
content: "This note contains the word pages in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note3.ok()).toBeTruthy();
|
||||||
|
const note3Data = await note3.json();
|
||||||
|
createdNoteIds.push(note3Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 4: Contains "homepage" (contains "page", not exact match)
|
||||||
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Homepage Note",
|
||||||
|
content: "This note is about homepage content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note4.ok()).toBeTruthy();
|
||||||
|
const note4Data = await note4.json();
|
||||||
|
createdNoteIds.push(note4Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 5: Another note with exact "pagio" in content
|
||||||
|
const note5 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Another pagio Note",
|
||||||
|
content: "This is another note with pagio content for testing exact matches.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note5.ok()).toBeTruthy();
|
||||||
|
const note5Data = await note5.json();
|
||||||
|
createdNoteIds.push(note5Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 6: Contains "pagio" in title only
|
||||||
|
const note6 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "pagio",
|
||||||
|
content: "This note has pagio as the title.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note6.ok()).toBeTruthy();
|
||||||
|
const note6Data = await note6.json();
|
||||||
|
createdNoteIds.push(note6Data.note.noteId);
|
||||||
|
|
||||||
|
// Wait a bit for indexing
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }) => {
|
||||||
|
// Clean up created notes
|
||||||
|
for (const noteId of createdNoteIds) {
|
||||||
|
try {
|
||||||
|
const taskId = `cleanup-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
await page.request.delete(`${BASE_URL}/api/notes/${noteId}?taskId=${taskId}&last=true`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to delete note ${noteId}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createdNoteIds = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Quick search without = operator returns all partial matches", async ({ page }) => {
|
||||||
|
// Test the /quick-search endpoint without the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/pag`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return multiple notes including "page", "pages", "homepage"
|
||||||
|
expect(data.searchResultNoteIds).toBeDefined();
|
||||||
|
expect(data.searchResults).toBeDefined();
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testResults = data.searchResults.filter((result: any) =>
|
||||||
|
result.noteTitle.includes("page") ||
|
||||||
|
result.noteTitle.includes("pagio") ||
|
||||||
|
result.noteTitle.includes("Homepage")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should find at least "page", "pages", "homepage", and "pagio" notes
|
||||||
|
expect(testResults.length).toBeGreaterThanOrEqual(4);
|
||||||
|
|
||||||
|
console.log("Quick search 'pag' found:", testResults.length, "matching notes");
|
||||||
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Quick search with = operator returns only exact matches", async ({ page }) => {
|
||||||
|
// Test the /quick-search endpoint WITH the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=pagio`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return only notes with exact "pagio" match
|
||||||
|
expect(data.searchResultNoteIds).toBeDefined();
|
||||||
|
expect(data.searchResults).toBeDefined();
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testResults = data.searchResults.filter((result: any) =>
|
||||||
|
createdNoteIds.includes(result.notePath.split("/").pop() || "")
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Quick search '=pagio' found:", testResults.length, "matching notes");
|
||||||
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find exactly 3 notes: "Test Note with pagio", "Another pagio Note", "pagio"
|
||||||
|
expect(testResults.length).toBe(3);
|
||||||
|
|
||||||
|
// Verify that none of the results contain "page" or "pages" (only "pagio")
|
||||||
|
for (const result of testResults) {
|
||||||
|
const title = result.noteTitle.toLowerCase();
|
||||||
|
const hasPageNotPagio = (title.includes("page") && !title.includes("pagio"));
|
||||||
|
expect(hasPageNotPagio).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Full search API without = operator returns partial matches", async ({ page }) => {
|
||||||
|
// Test the /search endpoint without the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/search/pag`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return an array of note IDs
|
||||||
|
expect(Array.isArray(data)).toBe(true);
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
||||||
|
|
||||||
|
console.log("Full search 'pag' found:", testNoteIds.length, "matching notes from our test set");
|
||||||
|
|
||||||
|
// Should find at least 4 notes
|
||||||
|
expect(testNoteIds.length).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Full search API with = operator returns only exact matches", async ({ page }) => {
|
||||||
|
// Test the /search endpoint WITH the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/search/=pagio`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return an array of note IDs
|
||||||
|
expect(Array.isArray(data)).toBe(true);
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
||||||
|
|
||||||
|
console.log("Full search '=pagio' found:", testNoteIds.length, "matching notes from our test set");
|
||||||
|
|
||||||
|
// Should find exactly 3 notes with exact "pagio" match
|
||||||
|
expect(testNoteIds.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact search operator works with content search", async ({ page }) => {
|
||||||
|
// Create a note with "test" in title but different content
|
||||||
|
const noteWithTest = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing Content",
|
||||||
|
content: "This note contains the exact word test in content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteWithTest.ok()).toBeTruthy();
|
||||||
|
const noteWithTestData = await noteWithTest.json();
|
||||||
|
const testNoteId = noteWithTestData.note.noteId;
|
||||||
|
createdNoteIds.push(testNoteId);
|
||||||
|
|
||||||
|
// Create a note with "testing" (not exact match)
|
||||||
|
const noteWithTesting = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing More",
|
||||||
|
content: "This note has testing in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteWithTesting.ok()).toBeTruthy();
|
||||||
|
const noteWithTestingData = await noteWithTesting.json();
|
||||||
|
createdNoteIds.push(noteWithTestingData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search with exact operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=test`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === testNoteId || noteId === noteWithTestingData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact search '=test' found our test notes:", ourTestNotes.length);
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find the note with exact "test" match, but not "testing"
|
||||||
|
// Note: This test may fail if the implementation doesn't properly handle exact matching in content
|
||||||
|
expect(ourTestNotes.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact search is case-insensitive", async ({ page }) => {
|
||||||
|
// Create notes with different case variations
|
||||||
|
const noteUpper = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "EXACT MATCH",
|
||||||
|
content: "This note has EXACT in uppercase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteUpper.ok()).toBeTruthy();
|
||||||
|
const noteUpperData = await noteUpper.json();
|
||||||
|
createdNoteIds.push(noteUpperData.note.noteId);
|
||||||
|
|
||||||
|
const noteLower = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact match",
|
||||||
|
content: "This note has exact in lowercase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteLower.ok()).toBeTruthy();
|
||||||
|
const noteLowerData = await noteLower.json();
|
||||||
|
createdNoteIds.push(noteLowerData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search with exact operator in lowercase
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=exact`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === noteUpperData.note.noteId || noteId === noteLowerData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Case-insensitive exact search found:", ourTestNotes.length, "notes");
|
||||||
|
|
||||||
|
// Should find both uppercase and lowercase versions
|
||||||
|
expect(ourTestNotes.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact phrase matching with multi-word searches", async ({ page }) => {
|
||||||
|
// Create notes with various phrase patterns
|
||||||
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact phrase",
|
||||||
|
content: "This note contains the exact phrase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note1.ok()).toBeTruthy();
|
||||||
|
const note1Data = await note1.json();
|
||||||
|
createdNoteIds.push(note1Data.note.noteId);
|
||||||
|
|
||||||
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact phrase match",
|
||||||
|
content: "This note has exact phrase followed by more words.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note2.ok()).toBeTruthy();
|
||||||
|
const note2Data = await note2.json();
|
||||||
|
createdNoteIds.push(note2Data.note.noteId);
|
||||||
|
|
||||||
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "phrase exact",
|
||||||
|
content: "This note has the words in reverse order.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note3.ok()).toBeTruthy();
|
||||||
|
const note3Data = await note3.json();
|
||||||
|
createdNoteIds.push(note3Data.note.noteId);
|
||||||
|
|
||||||
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "this exact and that phrase",
|
||||||
|
content: "Words are separated but both present.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note4.ok()).toBeTruthy();
|
||||||
|
const note4Data = await note4.json();
|
||||||
|
createdNoteIds.push(note4Data.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for exact phrase "exact phrase"
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='exact phrase'`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return [note1Data.note.noteId, note2Data.note.noteId, note3Data.note.noteId, note4Data.note.noteId].includes(noteId || "");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact phrase search '=\"exact phrase\"' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only notes 1 and 2 (consecutive "exact phrase")
|
||||||
|
// Should NOT find note 3 (reversed order) or note 4 (words separated)
|
||||||
|
expect(ourTestNotes.length).toBe(2);
|
||||||
|
|
||||||
|
const foundTitles = ourTestNotes.map((r: any) => r.noteTitle);
|
||||||
|
expect(foundTitles).toContain("exact phrase");
|
||||||
|
expect(foundTitles).toContain("exact phrase match");
|
||||||
|
expect(foundTitles).not.toContain("phrase exact");
|
||||||
|
expect(foundTitles).not.toContain("this exact and that phrase");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact phrase matching respects word order", async ({ page }) => {
|
||||||
|
// Create notes to test word order sensitivity
|
||||||
|
const noteForward = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing Order",
|
||||||
|
content: "This is a test sentence for verification.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteForward.ok()).toBeTruthy();
|
||||||
|
const noteForwardData = await noteForward.json();
|
||||||
|
createdNoteIds.push(noteForwardData.note.noteId);
|
||||||
|
|
||||||
|
const noteReverse = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Order Testing",
|
||||||
|
content: "A sentence test is this for verification.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteReverse.ok()).toBeTruthy();
|
||||||
|
const noteReverseData = await noteReverse.json();
|
||||||
|
createdNoteIds.push(noteReverseData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for exact phrase "test sentence"
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='test sentence'`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === noteForwardData.note.noteId || noteId === noteReverseData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact phrase search '=\"test sentence\"' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only the forward order note
|
||||||
|
expect(ourTestNotes.length).toBe(1);
|
||||||
|
expect(ourTestNotes[0].noteTitle).toBe("Testing Order");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multi-word exact search without quotes", async ({ page }) => {
|
||||||
|
// Test that multi-word search with = but without quotes also does exact phrase matching
|
||||||
|
const notePhrase = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Quick Test Note",
|
||||||
|
content: "A simple note for multi word testing.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(notePhrase.ok()).toBeTruthy();
|
||||||
|
const notePhraseData = await notePhrase.json();
|
||||||
|
createdNoteIds.push(notePhraseData.note.noteId);
|
||||||
|
|
||||||
|
const noteScattered = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Word Multi Testing",
|
||||||
|
content: "Words are multi scattered in this testing example.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteScattered.ok()).toBeTruthy();
|
||||||
|
const noteScatteredData = await noteScattered.json();
|
||||||
|
createdNoteIds.push(noteScatteredData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for "=multi word" without quotes (parser tokenizes as two words)
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=multi word`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === notePhraseData.note.noteId || noteId === noteScatteredData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Multi-word exact search '=multi word' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only the note with consecutive "multi word" phrase
|
||||||
|
expect(ourTestNotes.length).toBe(1);
|
||||||
|
expect(ourTestNotes[0].noteTitle).toBe("Quick Test Note");
|
||||||
|
});
|
||||||
|
});
|
||||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
51
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
51
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
@ -19,7 +19,8 @@ class="image image_resized" style="width:74.04%;">
|
|||||||
<img style="aspect-ratio:1911/997;" src="1_AI_image.png"
|
<img style="aspect-ratio:1911/997;" src="1_AI_image.png"
|
||||||
width="1911" height="997">
|
width="1911" height="997">
|
||||||
</figure>
|
</figure>
|
||||||
<h2>Embeddings</h2>
|
|
||||||
|
<h2>Embeddings</h2>
|
||||||
<p><strong>Embeddings</strong> are important as it allows us to have an compact
|
<p><strong>Embeddings</strong> are important as it allows us to have an compact
|
||||||
AI “summary” (it's not human readable text) of each of your Notes, that
|
AI “summary” (it's not human readable text) of each of your Notes, that
|
||||||
we can then perform mathematical functions on (such as cosine similarity)
|
we can then perform mathematical functions on (such as cosine similarity)
|
||||||
@ -79,59 +80,59 @@ class="image image_resized" style="width:74.04%;">
|
|||||||
<p>These are the tools that currently exist, and will certainly be updated
|
<p>These are the tools that currently exist, and will certainly be updated
|
||||||
to be more effectively (and even more to be added!):</p>
|
to be more effectively (and even more to be added!):</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e56ad2313afb3f2f5162bcb3d9bf8e963"><code>search_notes</code>
|
<li><code>search_notes</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="ee3b5cc2bd60f496e7919c63def713108">Semantic search</li>
|
<li>Semantic search</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e9abe7bae428aff060dfc70ef669bd079"><code>keyword_search</code>
|
<li><code>keyword_search</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="ea8e2cece0d711e80eacfdb42e5d0edc3">Keyword-based search</li>
|
<li>Keyword-based search</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e775735113778afe2073695881679540b"><code>attribute_search</code>
|
<li><code>attribute_search</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e64a93997e8ae8cc348e222f926866921">Attribute-specific search</li>
|
<li>Attribute-specific search</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e58d04b2e226a143a8c8941337b04113a"><code>search_suggestion</code>
|
<li><code>search_suggestion</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e7c1ed076f0e0e3c242ca659276576860">Search syntax helper</li>
|
<li>Search syntax helper</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e30741593cef85e71179301c280063bc2"><code>read_note</code>
|
<li><code>read_note</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e9c50f6fe0b4f013f16f9b32dbf6dbc92">Read note content (helps the LLM read Notes)</li>
|
<li>Read note content (helps the LLM read Notes)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="ec71fd71ce58b9d8338bc66e7c99ae1cb"><code>create_note</code>
|
<li><code>create_note</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e0b0bae68b45bb0c27bc092145452ecd1">Create a Note</li>
|
<li>Create a Note</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e7e816d84f52cf76096d2b5f1e4665989"><code>update_note</code>
|
<li><code>update_note</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="ed28fc8ca5a4753c1b680ee52d140940a">Update a Note</li>
|
<li>Update a Note</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e4580152b092501aa4fa87d1562b0f304"><code>manage_attributes</code>
|
<li><code>manage_attributes</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="ea0601e29b574cf0e4c40ae0e7d60bfa4">Manage attributes on a Note</li>
|
<li>Manage attributes on a Note</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="ecea045d4312b04ea06ac12802efb0544"><code>manage_relationships</code>
|
<li><code>manage_relationships</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e3293a7c119de1a0eb2ddf8779a0b0d65">Manage the various relationships between Notes</li>
|
<li>Manage the various relationships between Notes</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e483a2ef5f76d4da426c6059ddd3827a0"><code>extract_content</code>
|
<li><code>extract_content</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e1735854ab39d6d4e06a242bc2f80b839">Used to smartly extract content from a Note</li>
|
<li>Used to smartly extract content from a Note</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="e11df70f4a6846bf881cb110c2950065f"><code>calendar_integration</code>
|
<li><code>calendar_integration</code>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e0f1dfa6c5520bf02e125c1932b4f6e5e">Used to find date notes, create date notes, get the daily note, etc.</li>
|
<li>Used to find date notes, create date notes, get the daily note, etc.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -146,12 +147,12 @@ class="image image_resized" style="width:74.04%;">
|
|||||||
<h2>Overview</h2>
|
<h2>Overview</h2>
|
||||||
<p>To start, simply press the <em>Chat with Notes</em> button in the
|
<p>To start, simply press the <em>Chat with Notes</em> button in the
|
||||||
<a
|
<a
|
||||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>.</p>
|
class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>.</p>
|
||||||
<figure class="image image_resized" style="width:60.77%;">
|
<figure class="image image_resized" style="width:60.77%;">
|
||||||
<img style="aspect-ratio:1378/539;" src="2_AI_image.png"
|
<img style="aspect-ratio:1378/539;" src="2_AI_image.png"
|
||||||
width="1378" height="539">
|
width="1378" height="539">
|
||||||
</figure>
|
</figure>
|
||||||
<p>If you don't see the button in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>,
|
<p>If you don't see the button in the <a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>,
|
||||||
you might need to move it from the <em>Available Launchers</em> section to
|
you might need to move it from the <em>Available Launchers</em> section to
|
||||||
the <em>Visible Launchers</em> section:</p>
|
the <em>Visible Launchers</em> section:</p>
|
||||||
<figure class="image image_resized"
|
<figure class="image image_resized"
|
||||||
|
|||||||
@ -1,41 +1,40 @@
|
|||||||
<aside class="admonition warning">
|
<aside class="admonition warning">
|
||||||
<p>This functionality is still in preview, expect possible issues or even
|
<p>This functionality is still in preview, expect possible issues or even
|
||||||
the feature disappearing completely.
|
the feature disappearing completely.
|
||||||
<br>Feel free to <a href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_wy8So3yZZlH9">report</a> any
|
<br>Feel free to <a href="#root/_help_wy8So3yZZlH9">report</a> any issues you might
|
||||||
issues you might have.</p>
|
have.</p>
|
||||||
</aside>
|
</aside>
|
||||||
<p>The read-only database is an alternative to <a class="reference-link"
|
<p>The read-only database is an alternative to <a class="reference-link"
|
||||||
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_R9pX4DGra2Vt">Sharing</a> notes.
|
href="#root/_help_R9pX4DGra2Vt">Sharing</a> notes. Although the share functionality
|
||||||
Although the share functionality works pretty well to publish pages to
|
works pretty well to publish pages to the Internet in a wiki, blog-like
|
||||||
the Internet in a wiki, blog-like format it does not offer the full functionality
|
format it does not offer the full functionality behind Trilium (such as
|
||||||
behind Trilium (such as the advanced <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a> or
|
the advanced <a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a> or
|
||||||
the interactivity behind <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> or
|
the interactivity behind <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> or
|
||||||
the various <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_KSZ04uQ2D1St">Note Types</a>).</p>
|
the various <a class="reference-link" href="#root/_help_KSZ04uQ2D1St">Note Types</a>).</p>
|
||||||
<p>When the database is in read-only mode, the Trilium application can be
|
<p>When the database is in read-only mode, the Trilium application can be
|
||||||
used as normal, but editing is disabled and changes are made in-memory
|
used as normal, but editing is disabled and changes are made in-memory
|
||||||
only.</p>
|
only.</p>
|
||||||
<h2>What it does</h2>
|
<h2>What it does</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e97d71c2d01fa683d840e3f8de9e5bea9">All notes are read-only, without the possibility of editing them.</li>
|
<li>All notes are read-only, without the possibility of editing them.</li>
|
||||||
<li
|
<li>Features that would normally alter the database such as the list of recent
|
||||||
data-list-item-id="e6e82b25093f659cca922614ea8701ca4">Features that would normally alter the database such as the list of recent
|
|
||||||
notes are disabled.</li>
|
notes are disabled.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Limitations</h2>
|
<h2>Limitations</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="ec1a648467ed42eee3d6b96f829b6daad">Some features might “slip through” and still end up creating a note, for
|
<li>Some features might “slip through” and still end up creating a note, for
|
||||||
example.
|
example.
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="ed98de88bf24f574cfda28cf278be7f86">However, the database is still read-only, so all modifications will be
|
<li>However, the database is still read-only, so all modifications will be
|
||||||
reset if the server is restarted.</li>
|
reset if the server is restarted.</li>
|
||||||
<li data-list-item-id="e3f0450830304fa98038a8a5fd9026b06">Whenever this occurs, <code>ERROR: read-only DB ignored</code> will be shown
|
<li>Whenever this occurs, <code>ERROR: read-only DB ignored</code> will be shown
|
||||||
in the logs.</li>
|
in the logs.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Setting a database as read-only</h2>
|
<h2>Setting a database as read-only</h2>
|
||||||
<p>First, make sure the database is initialized (e.g. the first set up is
|
<p>First, make sure the database is initialized (e.g. the first set up is
|
||||||
complete). Then modify the <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_Gzjqa934BdH4">config.ini</a> by
|
complete). Then modify the <a href="#root/_help_Gzjqa934BdH4">config.ini</a> by
|
||||||
looking for the <code>[General]</code> section and adding a new <code>readOnly</code> field:</p><pre><code class="language-text-x-trilium-auto">[General]
|
looking for the <code>[General]</code> section and adding a new <code>readOnly</code> field:</p><pre><code class="language-text-x-trilium-auto">[General]
|
||||||
readOnly=true</code></pre>
|
readOnly=true</code></pre>
|
||||||
<p>If your server is already running, restart it to apply the changes.</p>
|
<p>If your server is already running, restart it to apply the changes.</p>
|
||||||
|
|||||||
@ -41,6 +41,15 @@
|
|||||||
<li>The export requires a functional web server as the pages will not render
|
<li>The export requires a functional web server as the pages will not render
|
||||||
properly if accessed locally via a web browser due to the use of module
|
properly if accessed locally via a web browser due to the use of module
|
||||||
scripts.</li>
|
scripts.</li>
|
||||||
|
<li>The directory structure is also slightly different:
|
||||||
|
<ul>
|
||||||
|
<li>A normal HTML export results in an index file and a single directory.</li>
|
||||||
|
<li>Instead, for static exporting the top-root level becomes the index file
|
||||||
|
and the child directories are on the root instead.</li>
|
||||||
|
<li>This makes it possible to easily publish to a website, without forcing
|
||||||
|
everything but the root note to be in a sub-directory.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Testing locally</h2>
|
<h2>Testing locally</h2>
|
||||||
<p>As mentioned previously, the exported static pages require a website to
|
<p>As mentioned previously, the exported static pages require a website to
|
||||||
@ -1,12 +1,12 @@
|
|||||||
<p>Data directory contains:</p>
|
<p>Data directory contains:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e02c8e6918e645272972d94bf51bd34e1"><code>document.db</code> - <a href="#root/_help_wX4HbRucYSDD">database</a>
|
<li><code>document.db</code> - <a href="#root/_help_wX4HbRucYSDD">database</a>
|
||||||
</li>
|
</li>
|
||||||
<li data-list-item-id="ee3b386f752a4d2de88a1362ff4bc447a"><code>config.ini</code> - instance level settings like port on which the
|
<li><code>config.ini</code> - instance level settings like port on which the
|
||||||
Trilium application runs</li>
|
Trilium application runs</li>
|
||||||
<li data-list-item-id="e9acb98fe27a493914c7f61574b277d59"><code>backup</code> - contains automatically <a href="#root/_help_ODY7qQn5m2FT">backup</a> of
|
<li><code>backup</code> - contains automatically <a href="#root/_help_ODY7qQn5m2FT">backup</a> of
|
||||||
documents</li>
|
documents</li>
|
||||||
<li data-list-item-id="e9f5de6d3c7f8d79710eb8c8e2ccac979"><code>log</code> - contains application log files</li>
|
<li><code>log</code> - contains application log files</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Location of the data directory</h2>
|
<h2>Location of the data directory</h2>
|
||||||
<p>Easy way how to find out which data directory Trilium uses is to look
|
<p>Easy way how to find out which data directory Trilium uses is to look
|
||||||
@ -18,14 +18,11 @@
|
|||||||
<p>Data directory is normally named <code>trilium-data</code> and it is stored
|
<p>Data directory is normally named <code>trilium-data</code> and it is stored
|
||||||
in:</p>
|
in:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e52706d339ce2332e73c5cfdc51edf81d"><code>/home/[user]/.local/share</code> for Linux</li>
|
<li><code>/home/[user]/.local/share</code> for Linux</li>
|
||||||
<li data-list-item-id="e5709c767479c028d2ef60d17124ea924"><code>C:\Users\[user]\AppData\Roaming</code> for Windows Vista and up</li>
|
<li><code>C:\Users\[user]\AppData\Roaming</code> for Windows Vista and up</li>
|
||||||
<li
|
<li><code>/Users/[user]/Library/Application Support</code> for Mac OS</li>
|
||||||
data-list-item-id="eae590672aea8b24e625f372fb22d1f1b"><code>/Users/[user]/Library/Application Support</code> for Mac OS</li>
|
<li>user's home is a fallback if some of the paths above don't exist</li>
|
||||||
<li
|
<li>user's home is also a default setup for [[docker|Docker server installation]]</li>
|
||||||
data-list-item-id="e150ab7fc3c436d7cbac192acf3fe433f">user's home is a fallback if some of the paths above don't exist</li>
|
|
||||||
<li
|
|
||||||
data-list-item-id="e5f476ac39d8fa453e19b8399073a79d7">user's home is also a default setup for [[docker|Docker server installation]]</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>If you want to back up your Trilium data, just backup this single directory
|
<p>If you want to back up your Trilium data, just backup this single directory
|
||||||
- it contains everything you need.</p>
|
- it contains everything you need.</p>
|
||||||
@ -35,17 +32,15 @@
|
|||||||
variable to some other location:</p>
|
variable to some other location:</p>
|
||||||
<h3>Windows</h3>
|
<h3>Windows</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li data-list-item-id="e8597645795a4e5b079b8e2e4a772a7d7">Press the Windows key on your keyboard.</li>
|
<li>Press the Windows key on your keyboard.</li>
|
||||||
<li data-list-item-id="ee32856df1db7c2d206baa480a672eeac">Search and select “Edit the system variables”.</li>
|
<li>Search and select “Edit the system variables”.</li>
|
||||||
<li data-list-item-id="e08c7cdc0659b414465a07c78dfdaeb34">Press the “Environment Variables…” button in the bottom-right of the newly
|
<li>Press the “Environment Variables…” button in the bottom-right of the newly
|
||||||
opened screen.</li>
|
opened screen.</li>
|
||||||
<li data-list-item-id="e6c7bae2590161a2661b6229c0853021c">On the top section ("User variables for [user]"), press the “New…” button.</li>
|
<li>On the top section ("User variables for [user]"), press the “New…” button.</li>
|
||||||
<li
|
<li>In the <em>Variable name</em> field insert <code>TRILIUM_DATA_DIR</code>.</li>
|
||||||
data-list-item-id="effac7dc3976f17d309958dd3374cfa8d">In the <em>Variable name</em> field insert <code>TRILIUM_DATA_DIR</code>.</li>
|
<li>Press the <em>Browse Directory…</em> button and select the new directory
|
||||||
<li
|
where to store the database.</li>
|
||||||
data-list-item-id="e3b74fdd03536563bb461e11111fae5ff">Press the <em>Browse Directory…</em> button and select the new directory
|
<li>Close all the windows by pressing the <em>OK</em> button for each of them.</li>
|
||||||
where to store the database.</li>
|
|
||||||
<li data-list-item-id="ed6df567f0fa772a14610b3904a0cb635">Close all the windows by pressing the <em>OK</em> button for each of them.</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
<h4>Linux</h4><pre><code class="language-text-x-trilium-auto">export TRILIUM_DATA_DIR=/home/myuser/data/my-trilium-data</code></pre>
|
<h4>Linux</h4><pre><code class="language-text-x-trilium-auto">export TRILIUM_DATA_DIR=/home/myuser/data/my-trilium-data</code></pre>
|
||||||
<h4>Mac OS X</h4>
|
<h4>Mac OS X</h4>
|
||||||
@ -74,63 +69,61 @@
|
|||||||
<h2>Fine-grained directory/path location</h2>
|
<h2>Fine-grained directory/path location</h2>
|
||||||
<p>Apart from the data directory, some of the subdirectories of it can be
|
<p>Apart from the data directory, some of the subdirectories of it can be
|
||||||
moved elsewhere by changing an environment variable:</p>
|
moved elsewhere by changing an environment variable:</p>
|
||||||
<figure class="table">
|
<table>
|
||||||
<table>
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th>Environment variable</th>
|
||||||
<th>Environment variable</th>
|
<th>Default value</th>
|
||||||
<th>Default value</th>
|
<th>Description</th>
|
||||||
<th>Description</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<td><code>TRILIUM_DOCUMENT_PATH</code>
|
||||||
<td><code>TRILIUM_DOCUMENT_PATH</code>
|
</td>
|
||||||
</td>
|
<td><code>${TRILIUM_DATA_DIR}/document.db</code>
|
||||||
<td><code>${TRILIUM_DATA_DIR}/document.db</code>
|
</td>
|
||||||
</td>
|
<td>Path to the <a class="reference-link" href="#root/_help_wX4HbRucYSDD">Database</a> (storing
|
||||||
<td>Path to the <a class="reference-link" href="#root/_help_wX4HbRucYSDD">Database</a> (storing
|
all notes and metadata).</td>
|
||||||
all notes and metadata).</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td><code>TRILIUM_BACKUP_DIR</code>
|
||||||
<td><code>TRILIUM_BACKUP_DIR</code>
|
</td>
|
||||||
</td>
|
<td><code>${TRILIUM_DATA_DIR}/backup</code>
|
||||||
<td><code>${TRILIUM_DATA_DIR}/backup</code>
|
</td>
|
||||||
</td>
|
<td>Directory where automated <a class="reference-link" href="#root/_help_ODY7qQn5m2FT">Backup</a> databases
|
||||||
<td>Directory where automated <a class="reference-link" href="#root/_help_ODY7qQn5m2FT">Backup</a> databases
|
are stored.</td>
|
||||||
are stored.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td><code>TRILIUM_LOG_DIR</code>
|
||||||
<td><code>TRILIUM_LOG_DIR</code>
|
</td>
|
||||||
</td>
|
<td><code>${TRILIUM_DATA_DIR}/log</code>
|
||||||
<td><code>${TRILIUM_DATA_DIR}/log</code>
|
</td>
|
||||||
</td>
|
<td>Directory where daily <a class="reference-link" href="#root/_help_bnyigUA2UK7s">Backend (server) logs</a> are
|
||||||
<td>Directory where daily <a class="reference-link" href="#root/_help_bnyigUA2UK7s">Backend (server) logs</a> are
|
stored.</td>
|
||||||
stored.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td><code>TRILIUM_TMP_DIR</code>
|
||||||
<td><code>TRILIUM_TMP_DIR</code>
|
</td>
|
||||||
</td>
|
<td><code>${TRILIUM_DATA_DIR}/tmp</code>
|
||||||
<td><code>${TRILIUM_DATA_DIR}/tmp</code>
|
</td>
|
||||||
</td>
|
<td>Directory where temporary files are stored (for example when opening in
|
||||||
<td>Directory where temporary files are stored (for example when opening in
|
an external app).</td>
|
||||||
an external app).</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td><code>TRILIUM_ANONYMIZED_DB_DIR</code>
|
||||||
<td><code>TRILIUM_ANONYMIZED_DB_DIR</code>
|
</td>
|
||||||
</td>
|
<td><code>${TRILIUM_DATA_DIR}/anonymized-db</code>
|
||||||
<td><code>${TRILIUM_DATA_DIR}/anonymized-db</code>
|
</td>
|
||||||
</td>
|
<td>Directory where a <a class="reference-link" href="#root/_help_x59R8J8KV5Bp">Anonymized Database</a> is
|
||||||
<td>Directory where a <a class="reference-link" href="#root/_help_x59R8J8KV5Bp">Anonymized Database</a> is
|
stored.</td>
|
||||||
stored.</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td><code>TRILIUM_CONFIG_INI_PATH</code>
|
||||||
<td><code>TRILIUM_CONFIG_INI_PATH</code>
|
</td>
|
||||||
</td>
|
<td><code>${TRILIUM_DATA_DIR}/config.ini</code>
|
||||||
<td><code>${TRILIUM_DATA_DIR}/config.ini</code>
|
</td>
|
||||||
</td>
|
<td>Path to <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a> file.</td>
|
||||||
<td>Path to <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a> file.</td>
|
</tr>
|
||||||
</tr>
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
|
||||||
</figure>
|
|
||||||
@ -8,7 +8,7 @@
|
|||||||
get parentWidget() { return "left-pane"; }
|
get parentWidget() { return "left-pane"; }
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $("");
|
this.$widget = $("<div id='my-widget'>");
|
||||||
return this.$widget;
|
return this.$widget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,13 +22,13 @@ module.exports = new MyWidget();</code></pre>
|
|||||||
the <a href="#root/_help_BFs8mudNFgCS">note</a>.</li>
|
the <a href="#root/_help_BFs8mudNFgCS">note</a>.</li>
|
||||||
<li>Restart Trilium or reload the window.</li>
|
<li>Restart Trilium or reload the window.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p>To verify that the widget is working, open the developer tools (<code>Cmd</code> + <code>Shift</code> + <code>I</code>)
|
<p>To verify that the widget is working, open the developer tools (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>)
|
||||||
and run <code>document.querySelector("#my-widget")</code>. If the element
|
and run <code>document.querySelector("#my-widget")</code>. If the element
|
||||||
is found, the widget is functioning correctly. If <code>undefined</code> is
|
is found, the widget is functioning correctly. If <code>undefined</code> is
|
||||||
returned, double-check that the <a href="#root/_help_BFs8mudNFgCS">note</a> has
|
returned, double-check that the <a href="#root/_help_BFs8mudNFgCS">note</a> has
|
||||||
the <code>#widget</code> <a href="#root/_help_zEY4DaJG4YT5">attribute</a>.</p>
|
the <code>#widget</code> <a href="#root/_help_zEY4DaJG4YT5">attribute</a>.</p>
|
||||||
<h3>Step 2: Adding an UI Element</h3>
|
<h3>Step 2: Adding an UI Element</h3>
|
||||||
<p>Next, let's improve the widget by adding a button to it.</p><pre><code class="language-text-x-trilium-auto">const template = ``;
|
<p>Next, let's improve the widget by adding a button to it.</p><pre><code class="language-text-x-trilium-auto">const template = `<div id="my-widget"><button>Click Me!</button></div>`;
|
||||||
|
|
||||||
class MyWidget extends api.BasicWidget {
|
class MyWidget extends api.BasicWidget {
|
||||||
get position() {return 1;}
|
get position() {return 1;}
|
||||||
@ -47,7 +47,7 @@ module.exports = new MyWidget();</code></pre>
|
|||||||
<p>To make the button more visually appealing and position it correctly,
|
<p>To make the button more visually appealing and position it correctly,
|
||||||
we'll apply some custom styling. Trilium includes <a href="https://boxicons.com">Box Icons</a>,
|
we'll apply some custom styling. Trilium includes <a href="https://boxicons.com">Box Icons</a>,
|
||||||
which we'll use to replace the button text with an icon. For example the <code>bx bxs-magic-wand</code> icon.</p>
|
which we'll use to replace the button text with an icon. For example the <code>bx bxs-magic-wand</code> icon.</p>
|
||||||
<p>Here's the updated template:</p><pre><code class="language-text-x-trilium-auto">const template = ``;</code></pre>
|
<p>Here's the updated template:</p><pre><code class="language-text-x-trilium-auto">const template = `<div id="my-widget"><button class="tree-floating-button bx bxs-magic-wand tree-settings-button"></button></div>`;</code></pre>
|
||||||
<p>Next, we'll adjust the button's position using CSS:</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.BasicWidget {
|
<p>Next, we'll adjust the button's position using CSS:</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.BasicWidget {
|
||||||
get position() { return 1; }
|
get position() { return 1; }
|
||||||
get parentWidget() { return "left-pane"; }
|
get parentWidget() { return "left-pane"; }
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import cls from "../../services/cls.js";
|
|||||||
import attributeFormatter from "../../services/attribute_formatter.js";
|
import attributeFormatter from "../../services/attribute_formatter.js";
|
||||||
import ValidationError from "../../errors/validation_error.js";
|
import ValidationError from "../../errors/validation_error.js";
|
||||||
import type SearchResult from "../../services/search/search_result.js";
|
import type SearchResult from "../../services/search/search_result.js";
|
||||||
|
import hoistedNoteService from "../../services/hoisted_note.js";
|
||||||
|
import beccaService from "../../becca/becca_service.js";
|
||||||
|
|
||||||
function searchFromNote(req: Request): SearchNoteResult {
|
function searchFromNote(req: Request): SearchNoteResult {
|
||||||
const note = becca.getNoteOrThrow(req.params.noteId);
|
const note = becca.getNoteOrThrow(req.params.noteId);
|
||||||
@ -49,13 +51,41 @@ function quickSearch(req: Request) {
|
|||||||
const searchContext = new SearchContext({
|
const searchContext = new SearchContext({
|
||||||
fastSearch: false,
|
fastSearch: false,
|
||||||
includeArchivedNotes: false,
|
includeArchivedNotes: false,
|
||||||
fuzzyAttributeSearch: false
|
includeHiddenNotes: true,
|
||||||
|
fuzzyAttributeSearch: true,
|
||||||
|
ignoreInternalAttributes: true,
|
||||||
|
ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree() ? "root" : hoistedNoteService.getHoistedNoteId()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute search with our context
|
||||||
|
const allSearchResults = searchService.findResultsWithQuery(searchString, searchContext);
|
||||||
|
const trimmed = allSearchResults.slice(0, 200);
|
||||||
|
|
||||||
|
// Extract snippets using highlightedTokens from our context
|
||||||
|
for (const result of trimmed) {
|
||||||
|
result.contentSnippet = searchService.extractContentSnippet(result.noteId, searchContext.highlightedTokens);
|
||||||
|
result.attributeSnippet = searchService.extractAttributeSnippet(result.noteId, searchContext.highlightedTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight the results
|
||||||
|
searchService.highlightSearchResults(trimmed, searchContext.highlightedTokens, searchContext.ignoreInternalAttributes);
|
||||||
|
|
||||||
|
// Map to API format
|
||||||
|
const searchResults = trimmed.map((result) => {
|
||||||
|
const { title, icon } = beccaService.getNoteTitleAndIcon(result.noteId);
|
||||||
|
return {
|
||||||
|
notePath: result.notePath,
|
||||||
|
noteTitle: title,
|
||||||
|
notePathTitle: result.notePathTitle,
|
||||||
|
highlightedNotePathTitle: result.highlightedNotePathTitle,
|
||||||
|
contentSnippet: result.contentSnippet,
|
||||||
|
highlightedContentSnippet: result.highlightedContentSnippet,
|
||||||
|
attributeSnippet: result.attributeSnippet,
|
||||||
|
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
|
||||||
|
icon: icon
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the same highlighting logic as autocomplete for consistency
|
|
||||||
const searchResults = searchService.searchNotesForAutocomplete(searchString, false);
|
|
||||||
|
|
||||||
// Extract note IDs for backward compatibility
|
|
||||||
const resultNoteIds = searchResults.map((result) => result.notePath.split("/").pop()).filter(Boolean) as string[];
|
const resultNoteIds = searchResults.map((result) => result.notePath.split("/").pop()).filter(Boolean) as string[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -75,20 +75,101 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
return inputNoteSet;
|
return inputNoteSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add tokens to highlightedTokens so snippet extraction knows what to look for
|
||||||
|
for (const token of this.tokens) {
|
||||||
|
if (!searchContext.highlightedTokens.includes(token)) {
|
||||||
|
searchContext.highlightedTokens.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resultNoteSet = new NoteSet();
|
const resultNoteSet = new NoteSet();
|
||||||
|
|
||||||
|
// Search through notes with content
|
||||||
for (const row of sql.iterateRows<SearchRow>(`
|
for (const row of sql.iterateRows<SearchRow>(`
|
||||||
SELECT noteId, type, mime, content, isProtected
|
SELECT noteId, type, mime, content, isProtected
|
||||||
FROM notes JOIN blobs USING (blobId)
|
FROM notes JOIN blobs USING (blobId)
|
||||||
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||||
AND isDeleted = 0
|
AND isDeleted = 0
|
||||||
AND LENGTH(content) < ${MAX_SEARCH_CONTENT_SIZE}`)) {
|
AND LENGTH(content) < ${MAX_SEARCH_CONTENT_SIZE}`)) {
|
||||||
this.findInText(row, inputNoteSet, resultNoteSet);
|
this.findInText(row, inputNoteSet, resultNoteSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For exact match with flatText, also search notes WITHOUT content (they may have matching attributes)
|
||||||
|
if (this.flatText && (this.operator === "=" || this.operator === "!=")) {
|
||||||
|
for (const note of inputNoteSet.notes) {
|
||||||
|
// Skip if already found or doesn't exist
|
||||||
|
if (resultNoteSet.hasNoteId(note.noteId) || !(note.noteId in becca.notes)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteFromBecca = becca.notes[note.noteId];
|
||||||
|
const flatText = noteFromBecca.getFlatText();
|
||||||
|
|
||||||
|
// For flatText, only check attribute values (format: #name=value or ~name=value)
|
||||||
|
// Don't match against noteId, type, mime, or title which are also in flatText
|
||||||
|
let matches = false;
|
||||||
|
const phrase = this.tokens.join(" ");
|
||||||
|
const normalizedPhrase = normalizeSearchText(phrase);
|
||||||
|
const normalizedFlatText = normalizeSearchText(flatText);
|
||||||
|
|
||||||
|
// Check if =phrase appears in flatText (indicates attribute value match)
|
||||||
|
matches = normalizedFlatText.includes(`=${normalizedPhrase}`);
|
||||||
|
|
||||||
|
if ((this.operator === "=" && matches) || (this.operator === "!=" && !matches)) {
|
||||||
|
resultNoteSet.add(noteFromBecca);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return resultNoteSet;
|
return resultNoteSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if content contains the exact word (with word boundaries) or exact phrase
|
||||||
|
* This is case-insensitive since content and token are already normalized
|
||||||
|
*/
|
||||||
|
private containsExactWord(token: string, content: string): boolean {
|
||||||
|
// Normalize both for case-insensitive comparison
|
||||||
|
const normalizedToken = normalizeSearchText(token);
|
||||||
|
const normalizedContent = normalizeSearchText(content);
|
||||||
|
|
||||||
|
// If token contains spaces, it's a multi-word phrase from quotes
|
||||||
|
// Check for substring match (consecutive phrase)
|
||||||
|
if (normalizedToken.includes(' ')) {
|
||||||
|
return normalizedContent.includes(normalizedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single words, split content into words and check for exact match
|
||||||
|
const words = normalizedContent.split(/\s+/);
|
||||||
|
return words.some(word => word === normalizedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if content contains the exact phrase (consecutive words in order)
|
||||||
|
* This is case-insensitive since content and tokens are already normalized
|
||||||
|
*/
|
||||||
|
private containsExactPhrase(tokens: string[], content: string, checkFlatTextAttributes: boolean = false): boolean {
|
||||||
|
const normalizedTokens = tokens.map(t => normalizeSearchText(t));
|
||||||
|
const normalizedContent = normalizeSearchText(content);
|
||||||
|
|
||||||
|
// Join tokens with single space to form the phrase
|
||||||
|
const phrase = normalizedTokens.join(" ");
|
||||||
|
|
||||||
|
// Check if the phrase appears as a substring (consecutive words)
|
||||||
|
if (normalizedContent.includes(phrase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For flatText, also check if the phrase appears in attribute values
|
||||||
|
// Attributes in flatText appear as "#name=value" or "~name=value"
|
||||||
|
// So we need to check for "=phrase" to match attribute values
|
||||||
|
if (checkFlatTextAttributes && normalizedContent.includes(`=${phrase}`)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
findInText({ noteId, isProtected, content, type, mime }: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
|
findInText({ noteId, isProtected, content, type, mime }: SearchRow, inputNoteSet: NoteSet, resultNoteSet: NoteSet) {
|
||||||
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
|
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
|
||||||
return;
|
return;
|
||||||
@ -112,7 +193,7 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content = this.preprocessContent(content, type, mime);
|
content = this.preprocessContent(content, type, mime);
|
||||||
|
|
||||||
// Apply content size validation and preprocessing
|
// Apply content size validation and preprocessing
|
||||||
const processedContent = validateAndPreprocessContent(content, noteId);
|
const processedContent = validateAndPreprocessContent(content, noteId);
|
||||||
if (!processedContent) {
|
if (!processedContent) {
|
||||||
@ -123,9 +204,25 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
if (this.tokens.length === 1) {
|
if (this.tokens.length === 1) {
|
||||||
const [token] = this.tokens;
|
const [token] = this.tokens;
|
||||||
|
|
||||||
|
let matches = false;
|
||||||
|
if (this.operator === "=") {
|
||||||
|
matches = this.containsExactWord(token, content);
|
||||||
|
// Also check flatText if enabled (includes attributes)
|
||||||
|
if (!matches && this.flatText) {
|
||||||
|
const flatText = becca.notes[noteId].getFlatText();
|
||||||
|
matches = this.containsExactPhrase([token], flatText, true);
|
||||||
|
}
|
||||||
|
} else if (this.operator === "!=") {
|
||||||
|
matches = !this.containsExactWord(token, content);
|
||||||
|
// For negation, check flatText too
|
||||||
|
if (matches && this.flatText) {
|
||||||
|
const flatText = becca.notes[noteId].getFlatText();
|
||||||
|
matches = !this.containsExactPhrase([token], flatText, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(this.operator === "=" && token === content) ||
|
matches ||
|
||||||
(this.operator === "!=" && token !== content) ||
|
|
||||||
(this.operator === "*=" && content.endsWith(token)) ||
|
(this.operator === "*=" && content.endsWith(token)) ||
|
||||||
(this.operator === "=*" && content.startsWith(token)) ||
|
(this.operator === "=*" && content.startsWith(token)) ||
|
||||||
(this.operator === "*=*" && content.includes(token)) ||
|
(this.operator === "*=*" && content.includes(token)) ||
|
||||||
@ -138,10 +235,26 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
} else {
|
} else {
|
||||||
// Multi-token matching with fuzzy support and phrase proximity
|
// Multi-token matching with fuzzy support and phrase proximity
|
||||||
if (this.operator === "~=" || this.operator === "~*") {
|
if (this.operator === "~=" || this.operator === "~*") {
|
||||||
|
// Fuzzy phrase matching
|
||||||
if (this.matchesWithFuzzy(content, noteId)) {
|
if (this.matchesWithFuzzy(content, noteId)) {
|
||||||
resultNoteSet.add(becca.notes[noteId]);
|
resultNoteSet.add(becca.notes[noteId]);
|
||||||
}
|
}
|
||||||
|
} else if (this.operator === "=" || this.operator === "!=") {
|
||||||
|
// Exact phrase matching for = and !=
|
||||||
|
let matches = this.containsExactPhrase(this.tokens, content, false);
|
||||||
|
|
||||||
|
// Also check flatText if enabled (includes attributes)
|
||||||
|
if (!matches && this.flatText) {
|
||||||
|
const flatText = becca.notes[noteId].getFlatText();
|
||||||
|
matches = this.containsExactPhrase(this.tokens, flatText, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.operator === "=" && matches) ||
|
||||||
|
(this.operator === "!=" && !matches)) {
|
||||||
|
resultNoteSet.add(becca.notes[noteId]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Other operators: check all tokens present (any order)
|
||||||
const nonMatchingToken = this.tokens.find(
|
const nonMatchingToken = this.tokens.find(
|
||||||
(token) =>
|
(token) =>
|
||||||
!this.tokenMatchesContent(token, content, noteId)
|
!this.tokenMatchesContent(token, content, noteId)
|
||||||
|
|||||||
@ -13,8 +13,41 @@ function getRegex(str: string) {
|
|||||||
type Comparator<T> = (comparedValue: T) => (val: string) => boolean;
|
type Comparator<T> = (comparedValue: T) => (val: string) => boolean;
|
||||||
|
|
||||||
const stringComparators: Record<string, Comparator<string>> = {
|
const stringComparators: Record<string, Comparator<string>> = {
|
||||||
"=": (comparedValue) => (val) => val === comparedValue,
|
"=": (comparedValue) => (val) => {
|
||||||
"!=": (comparedValue) => (val) => val !== comparedValue,
|
// For the = operator, check if the value contains the exact word or phrase
|
||||||
|
// This is case-insensitive
|
||||||
|
if (!val) return false;
|
||||||
|
|
||||||
|
const normalizedVal = normalizeSearchText(val);
|
||||||
|
const normalizedCompared = normalizeSearchText(comparedValue);
|
||||||
|
|
||||||
|
// If comparedValue has spaces, it's a multi-word phrase
|
||||||
|
// Check for substring match (consecutive phrase)
|
||||||
|
if (normalizedCompared.includes(" ")) {
|
||||||
|
return normalizedVal.includes(normalizedCompared);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single word, split into words and check for exact word match
|
||||||
|
const words = normalizedVal.split(/\s+/);
|
||||||
|
return words.some(word => word === normalizedCompared);
|
||||||
|
},
|
||||||
|
"!=": (comparedValue) => (val) => {
|
||||||
|
// Negation of exact word/phrase match
|
||||||
|
if (!val) return true;
|
||||||
|
|
||||||
|
const normalizedVal = normalizeSearchText(val);
|
||||||
|
const normalizedCompared = normalizeSearchText(comparedValue);
|
||||||
|
|
||||||
|
// If comparedValue has spaces, it's a multi-word phrase
|
||||||
|
// Check for substring match (consecutive phrase) and negate
|
||||||
|
if (normalizedCompared.includes(" ")) {
|
||||||
|
return !normalizedVal.includes(normalizedCompared);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single word, split into words and check for exact word match, then negate
|
||||||
|
const words = normalizedVal.split(/\s+/);
|
||||||
|
return !words.some(word => word === normalizedCompared);
|
||||||
|
},
|
||||||
">": (comparedValue) => (val) => val > comparedValue,
|
">": (comparedValue) => (val) => val > comparedValue,
|
||||||
">=": (comparedValue) => (val) => val >= comparedValue,
|
">=": (comparedValue) => (val) => val >= comparedValue,
|
||||||
"<": (comparedValue) => (val) => val < comparedValue,
|
"<": (comparedValue) => (val) => val < comparedValue,
|
||||||
|
|||||||
@ -38,11 +38,14 @@ function getFulltext(_tokens: TokenData[], searchContext: SearchContext, leading
|
|||||||
|
|
||||||
if (!searchContext.fastSearch) {
|
if (!searchContext.fastSearch) {
|
||||||
// For exact match with "=", we need different behavior
|
// For exact match with "=", we need different behavior
|
||||||
if (leadingOperator === "=" && tokens.length === 1) {
|
if (leadingOperator === "=" && tokens.length >= 1) {
|
||||||
// Exact match on title OR exact match on content
|
// Exact match on title OR exact match on content OR exact match in flat text (includes attributes)
|
||||||
|
// For multi-word, join tokens with space to form exact phrase
|
||||||
|
const titleSearchValue = tokens.join(" ");
|
||||||
return new OrExp([
|
return new OrExp([
|
||||||
new PropertyComparisonExp(searchContext, "title", "=", tokens[0]),
|
new PropertyComparisonExp(searchContext, "title", "=", titleSearchValue),
|
||||||
new NoteContentFulltextExp("=", { tokens, flatText: false })
|
new NoteContentFulltextExp("=", { tokens, flatText: false }),
|
||||||
|
new NoteContentFulltextExp("=", { tokens, flatText: true })
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp(operator, { tokens, flatText: true })]);
|
return new OrExp([new NoteFlatTextExp(tokens), new NoteContentFulltextExp(operator, { tokens, flatText: true })]);
|
||||||
|
|||||||
@ -242,18 +242,149 @@ describe("Search", () => {
|
|||||||
|
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
|
|
||||||
// Using leading = for exact title match
|
// Using leading = for exact word match - should find notes containing the exact word "example"
|
||||||
let searchResults = searchService.findResultsWithQuery("=Example Note", searchContext);
|
let searchResults = searchService.findResultsWithQuery("=example", searchContext);
|
||||||
expect(searchResults.length).toEqual(1);
|
expect(searchResults.length).toEqual(2); // "Example Note" and "Sample" (has label "example")
|
||||||
expect(findNoteByTitle(searchResults, "Example Note")).toBeTruthy();
|
expect(findNoteByTitle(searchResults, "Example Note")).toBeTruthy();
|
||||||
|
expect(findNoteByTitle(searchResults, "Sample")).toBeTruthy();
|
||||||
|
|
||||||
// Without =, it should find all notes containing "example"
|
// Without =, it should find all notes containing "example" (substring match)
|
||||||
searchResults = searchService.findResultsWithQuery("example", searchContext);
|
searchResults = searchService.findResultsWithQuery("example", searchContext);
|
||||||
expect(searchResults.length).toEqual(3);
|
expect(searchResults.length).toEqual(3); // All notes
|
||||||
|
|
||||||
// = operator should not match partial words
|
// = operator should not match partial words
|
||||||
searchResults = searchService.findResultsWithQuery("=Example", searchContext);
|
searchResults = searchService.findResultsWithQuery("=examples", searchContext);
|
||||||
expect(searchResults.length).toEqual(0);
|
expect(searchResults.length).toEqual(1); // Only "Examples of Usage"
|
||||||
|
expect(findNoteByTitle(searchResults, "Examples of Usage")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leading = operator for exact match - comprehensive title tests", () => {
|
||||||
|
// Create notes with varying titles to test exact vs contains matching
|
||||||
|
rootNote
|
||||||
|
.child(note("testing"))
|
||||||
|
.child(note("testing123"))
|
||||||
|
.child(note("My testing notes"))
|
||||||
|
.child(note("123testing"))
|
||||||
|
.child(note("test"));
|
||||||
|
|
||||||
|
const searchContext = new SearchContext();
|
||||||
|
|
||||||
|
// Test 1: Exact word match with leading = should find notes containing the exact word "testing"
|
||||||
|
let searchResults = searchService.findResultsWithQuery("=testing", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(2); // "testing" and "My testing notes" (word boundary)
|
||||||
|
expect(findNoteByTitle(searchResults, "testing")).toBeTruthy();
|
||||||
|
expect(findNoteByTitle(searchResults, "My testing notes")).toBeTruthy();
|
||||||
|
|
||||||
|
// Test 2: Without =, it should find all notes containing "testing" (substring contains behavior)
|
||||||
|
searchResults = searchService.findResultsWithQuery("testing", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(4); // All notes with "testing" substring
|
||||||
|
|
||||||
|
// Test 3: Exact match should only find the exact composite word
|
||||||
|
searchResults = searchService.findResultsWithQuery("=testing123", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1);
|
||||||
|
expect(findNoteByTitle(searchResults, "testing123")).toBeTruthy();
|
||||||
|
|
||||||
|
// Test 4: Exact match should only find the exact composite word
|
||||||
|
searchResults = searchService.findResultsWithQuery("=123testing", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1);
|
||||||
|
expect(findNoteByTitle(searchResults, "123testing")).toBeTruthy();
|
||||||
|
|
||||||
|
// Test 5: Verify that "test" doesn't match "testing" with exact search
|
||||||
|
searchResults = searchService.findResultsWithQuery("=test", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1);
|
||||||
|
expect(findNoteByTitle(searchResults, "test")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leading = operator with quoted phrases", () => {
|
||||||
|
rootNote
|
||||||
|
.child(note("exact phrase"))
|
||||||
|
.child(note("exact phrase match"))
|
||||||
|
.child(note("this exact phrase here"))
|
||||||
|
.child(note("phrase exact"));
|
||||||
|
|
||||||
|
const searchContext = new SearchContext();
|
||||||
|
|
||||||
|
// Test 1: With = and quotes, treat as exact phrase match (consecutive words in order)
|
||||||
|
let searchResults = searchService.findResultsWithQuery("='exact phrase'", searchContext);
|
||||||
|
// Should match only notes containing the exact phrase "exact phrase"
|
||||||
|
expect(searchResults.length).toEqual(3); // Only notes with consecutive "exact phrase"
|
||||||
|
expect(findNoteByTitle(searchResults, "exact phrase")).toBeTruthy();
|
||||||
|
expect(findNoteByTitle(searchResults, "exact phrase match")).toBeTruthy();
|
||||||
|
expect(findNoteByTitle(searchResults, "this exact phrase here")).toBeTruthy();
|
||||||
|
|
||||||
|
// Test 2: Without =, quoted phrase should find substring/contains matches
|
||||||
|
searchResults = searchService.findResultsWithQuery("'exact phrase'", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(3); // All notes containing the phrase substring
|
||||||
|
expect(findNoteByTitle(searchResults, "exact phrase")).toBeTruthy();
|
||||||
|
expect(findNoteByTitle(searchResults, "exact phrase match")).toBeTruthy();
|
||||||
|
expect(findNoteByTitle(searchResults, "this exact phrase here")).toBeTruthy();
|
||||||
|
|
||||||
|
// Test 3: Verify word order matters with exact phrase matching
|
||||||
|
searchResults = searchService.findResultsWithQuery("='phrase exact'", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1); // Only "phrase exact" matches
|
||||||
|
expect(findNoteByTitle(searchResults, "phrase exact")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leading = operator case sensitivity", () => {
|
||||||
|
rootNote
|
||||||
|
.child(note("TESTING"))
|
||||||
|
.child(note("testing"))
|
||||||
|
.child(note("Testing"))
|
||||||
|
.child(note("TeStiNg"));
|
||||||
|
|
||||||
|
const searchContext = new SearchContext();
|
||||||
|
|
||||||
|
// Exact match should be case-insensitive (based on lex.ts line 4: str.toLowerCase())
|
||||||
|
let searchResults = searchService.findResultsWithQuery("=testing", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(4); // All variants of "testing"
|
||||||
|
|
||||||
|
searchResults = searchService.findResultsWithQuery("=TESTING", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(4); // All variants
|
||||||
|
|
||||||
|
searchResults = searchService.findResultsWithQuery("=Testing", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(4); // All variants
|
||||||
|
|
||||||
|
searchResults = searchService.findResultsWithQuery("=TeStiNg", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(4); // All variants
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leading = operator with special characters", () => {
|
||||||
|
rootNote
|
||||||
|
.child(note("test-note"))
|
||||||
|
.child(note("test_note"))
|
||||||
|
.child(note("test.note"))
|
||||||
|
.child(note("test note"))
|
||||||
|
.child(note("testnote"));
|
||||||
|
|
||||||
|
const searchContext = new SearchContext();
|
||||||
|
|
||||||
|
// Each exact match should only find its specific variant (compound words are treated as single words)
|
||||||
|
let searchResults = searchService.findResultsWithQuery("=test-note", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1);
|
||||||
|
expect(findNoteByTitle(searchResults, "test-note")).toBeTruthy();
|
||||||
|
|
||||||
|
searchResults = searchService.findResultsWithQuery("=test_note", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1);
|
||||||
|
expect(findNoteByTitle(searchResults, "test_note")).toBeTruthy();
|
||||||
|
|
||||||
|
searchResults = searchService.findResultsWithQuery("=test.note", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1);
|
||||||
|
expect(findNoteByTitle(searchResults, "test.note")).toBeTruthy();
|
||||||
|
|
||||||
|
// For phrases with spaces, use quotes to keep them together
|
||||||
|
// With exact phrase matching, this finds notes with the consecutive phrase
|
||||||
|
searchResults = searchService.findResultsWithQuery("='test note'", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1); // Only "test note" has the exact phrase
|
||||||
|
expect(findNoteByTitle(searchResults, "test note")).toBeTruthy();
|
||||||
|
|
||||||
|
// Without quotes, "test note" is tokenized as two separate tokens
|
||||||
|
// and will be treated as an exact phrase search with = operator
|
||||||
|
searchResults = searchService.findResultsWithQuery("=test note", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(1); // Only "test note" has the exact phrase
|
||||||
|
|
||||||
|
// Without =, should find all matches containing "test" substring
|
||||||
|
searchResults = searchService.findResultsWithQuery("test", searchContext);
|
||||||
|
expect(searchResults.length).toEqual(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fuzzy attribute search", () => {
|
it("fuzzy attribute search", () => {
|
||||||
|
|||||||
@ -500,19 +500,38 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
|
|||||||
|
|
||||||
// Extract snippet
|
// Extract snippet
|
||||||
let snippet = content.substring(snippetStart, snippetStart + maxLength);
|
let snippet = content.substring(snippetStart, snippetStart + maxLength);
|
||||||
|
|
||||||
// If snippet contains linebreaks, limit to max 4 lines and override character limit
|
// If snippet contains linebreaks, limit to max 4 lines and override character limit
|
||||||
const lines = snippet.split('\n');
|
const lines = snippet.split('\n');
|
||||||
if (lines.length > 4) {
|
if (lines.length > 4) {
|
||||||
snippet = lines.slice(0, 4).join('\n');
|
// Find which lines contain the search tokens to ensure they're included
|
||||||
|
const normalizedLines = lines.map(line => normalizeString(line.toLowerCase()));
|
||||||
|
const normalizedTokens = searchTokens.map(token => normalizeString(token.toLowerCase()));
|
||||||
|
|
||||||
|
// Find the first line that contains a search token
|
||||||
|
let firstMatchLine = -1;
|
||||||
|
for (let i = 0; i < normalizedLines.length; i++) {
|
||||||
|
if (normalizedTokens.some(token => normalizedLines[i].includes(token))) {
|
||||||
|
firstMatchLine = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstMatchLine !== -1) {
|
||||||
|
// Center the 4-line window around the first match
|
||||||
|
// Try to show 1 line before and 2 lines after the match
|
||||||
|
const startLine = Math.max(0, firstMatchLine - 1);
|
||||||
|
const endLine = Math.min(lines.length, startLine + 4);
|
||||||
|
snippet = lines.slice(startLine, endLine).join('\n');
|
||||||
|
} else {
|
||||||
|
// No match found in lines (shouldn't happen), just take first 4
|
||||||
|
snippet = lines.slice(0, 4).join('\n');
|
||||||
|
}
|
||||||
// Add ellipsis if we truncated lines
|
// Add ellipsis if we truncated lines
|
||||||
snippet = snippet + "...";
|
snippet = snippet + "...";
|
||||||
} else if (lines.length > 1) {
|
} else if (lines.length > 1) {
|
||||||
// For multi-line snippets, just limit to 4 lines (keep existing snippet)
|
// For multi-line snippets that are 4 or fewer lines, keep them as-is
|
||||||
snippet = lines.slice(0, 4).join('\n');
|
// No need to truncate
|
||||||
if (lines.length > 4) {
|
|
||||||
snippet = snippet + "...";
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Single line content - apply original word boundary logic
|
// Single line content - apply original word boundary logic
|
||||||
// Try to start/end at word boundaries
|
// Try to start/end at word boundaries
|
||||||
@ -770,5 +789,8 @@ export default {
|
|||||||
searchNotesForAutocomplete,
|
searchNotesForAutocomplete,
|
||||||
findResultsWithQuery,
|
findResultsWithQuery,
|
||||||
findFirstNoteWithQuery,
|
findFirstNoteWithQuery,
|
||||||
searchNotes
|
searchNotes,
|
||||||
|
extractContentSnippet,
|
||||||
|
extractAttributeSnippet,
|
||||||
|
highlightSearchResults
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
"productivity_benefits": {
|
"productivity_benefits": {
|
||||||
"title": "Produktywność i bezpieczeństwo",
|
"title": "Produktywność i bezpieczeństwo",
|
||||||
"revisions_title": "Historia zmian",
|
"revisions_title": "Historia zmian",
|
||||||
"revisions_content": "Notatki są regularnie zapisywane w tle, co pozwala to przeglądać i cofać wprowadzone zmiany. Zapisy można także wykonywać \"na życzenie\".",
|
"revisions_content": "Notatki są regularnie zapisywane w tle, pozwala to przeglądać i cofać wprowadzone zmiany. Zapisy można także wykonywać \"na życzenie\".",
|
||||||
"sync_title": "Synchronizacja",
|
"sync_title": "Synchronizacja",
|
||||||
"sync_content": "Używaj własnych lub chmurowych instancji do łatwiejszej synchronizacji notatek między wieloma urządzeniami, w tym twoim telefonem używając PWA.",
|
"sync_content": "Używaj własnych lub chmurowych instancji do łatwiejszej synchronizacji notatek między wieloma urządzeniami, w tym twoim telefonem używając PWA.",
|
||||||
"protected_notes_title": "Notatki chronione",
|
"protected_notes_title": "Notatki chronione",
|
||||||
@ -51,7 +51,8 @@
|
|||||||
"mermaid_description": "Twórz diagramy, takie jak schematy blokowe, diagramy klas i sekwencyjne, wykresy Gantta i wiele innych, korzystając z składni Mermaid.",
|
"mermaid_description": "Twórz diagramy, takie jak schematy blokowe, diagramy klas i sekwencyjne, wykresy Gantta i wiele innych, korzystając z składni Mermaid.",
|
||||||
"mindmap_title": "Mapy myśli",
|
"mindmap_title": "Mapy myśli",
|
||||||
"mindmap_description": "Organizuj wizualnie swoje myśli albo przeprowadź sesję burzy mózgów.",
|
"mindmap_description": "Organizuj wizualnie swoje myśli albo przeprowadź sesję burzy mózgów.",
|
||||||
"others_list": "I wiele innych: <0>mapa notatek</0>, <1>mapa powiązań</1>, <2>zapisane wyszukiwania</2>, <3>renderowane notatki</3>, and <4>podgląd stron www</4>."
|
"others_list": "I wiele innych: <0>mapa notatek</0>, <1>mapa powiązań</1>, <2>zapisane wyszukiwania</2>, <3>renderowane notatki</3>, and <4>podgląd stron www</4>.",
|
||||||
|
"title": "Wiele sposobów przedstawienia Twoich informacji"
|
||||||
},
|
},
|
||||||
"extensibility_benefits": {
|
"extensibility_benefits": {
|
||||||
"title": "Udostępnianie i rozszerzenia",
|
"title": "Udostępnianie i rozszerzenia",
|
||||||
@ -69,10 +70,13 @@
|
|||||||
"calendar_description": "Organizuj swoje prywatne i służbowe wydarzenia używając kalendarza. Miej plany pod kontrolą z tygodniowym, miesięcznym i rocznym podglądem. Twórz i edytuj wydarzenia w prosty i intuicyjny sposób.",
|
"calendar_description": "Organizuj swoje prywatne i służbowe wydarzenia używając kalendarza. Miej plany pod kontrolą z tygodniowym, miesięcznym i rocznym podglądem. Twórz i edytuj wydarzenia w prosty i intuicyjny sposób.",
|
||||||
"table_title": "Tabele",
|
"table_title": "Tabele",
|
||||||
"table_description": "Wyświetlaj i edytuj informacje o notatkach w tabelach na wiele sposobów dzięki wielu typom kolumn: Tekstowym, numerycznym, z polami wyboru, z datami i godzinami, zawierającym linki, z kolorowymi wypełnieniami i powiązaniami notatek. Możesz nawet wyświetlić całe drzewo hierarchii w tabeli.",
|
"table_description": "Wyświetlaj i edytuj informacje o notatkach w tabelach na wiele sposobów dzięki wielu typom kolumn: Tekstowym, numerycznym, z polami wyboru, z datami i godzinami, zawierającym linki, z kolorowymi wypełnieniami i powiązaniami notatek. Możesz nawet wyświetlić całe drzewo hierarchii w tabeli.",
|
||||||
"board_title": "Tablice",
|
"board_title": "Tablice Kanban",
|
||||||
"board_description": "Organizuj swoje zadania i postępy projektów w tablicach Kanban z prostym tworzeniem nowych elementów i kolumn, a możliwość graficznego ich przenoszenia ułatwi zmianę statusu i pozwoli zachować porządek.",
|
"board_description": "Organizuj swoje zadania i postępy projektów w tablicach Kanban z prostym tworzeniem nowych elementów i kolumn, a możliwość graficznego ich przenoszenia ułatwi zmianę statusu i pozwoli zachować porządek.",
|
||||||
"geomap_title": "Mapy",
|
"geomap_title": "Mapy",
|
||||||
"geomap_description": "Zaplanuj wakacje albo interesujące miejsca bezpośrednio na mapie, używaj personalizowanych pinezek. Dzięki możliwości importu plików GPX możesz wyświetlać przebyte trasy."
|
"geomap_description": "Zaplanuj wakacje albo interesujące miejsca bezpośrednio na mapie, używaj personalizowanych pinezek. Dzięki możliwości importu plików GPX możesz wyświetlać przebyte trasy.",
|
||||||
|
"title": "Kolekcje",
|
||||||
|
"presentation_title": "Prezentacje",
|
||||||
|
"presentation_description": "Zawrzyj informacje w slajdach i zaprezentuj je w pełnoekranowych prezentacjach, które możesz łatwo wyeksportować do plików PDF."
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"title": "Częste pytania",
|
"title": "Częste pytania",
|
||||||
@ -187,5 +191,10 @@
|
|||||||
"description": "Trilium Notes hostowane na PikaPods, płatnym serwisie dla łatwego dostępu i zarządzania. Bezpośrednio nie związanie z Trilium team.",
|
"description": "Trilium Notes hostowane na PikaPods, płatnym serwisie dla łatwego dostępu i zarządzania. Bezpośrednio nie związanie z Trilium team.",
|
||||||
"download_pikapod": "Konfiguruj na PikaPods",
|
"download_pikapod": "Konfiguruj na PikaPods",
|
||||||
"download_triliumcc": "Alternatywnie patrz na trilium.cc"
|
"download_triliumcc": "Alternatywnie patrz na trilium.cc"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"get-started": "Start",
|
||||||
|
"documentation": "Dokumentacja",
|
||||||
|
"support-us": "Wesprzyj nas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
docs/Developer Guide/!!!meta.json
vendored
72
docs/Developer Guide/!!!meta.json
vendored
@ -245,6 +245,13 @@
|
|||||||
"value": "database",
|
"value": "database",
|
||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 20
|
"position": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"name": "iconClass",
|
||||||
|
"value": "bx bx-data",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 30
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
@ -765,6 +772,71 @@
|
|||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"dataFileName": "revisions.md",
|
"dataFileName": "revisions.md",
|
||||||
"attachments": []
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"isClone": false,
|
||||||
|
"noteId": "6DG1au6rgOTl",
|
||||||
|
"notePath": [
|
||||||
|
"jdjRLhLV3TtI",
|
||||||
|
"MhwWMgxwDTZL",
|
||||||
|
"pRZhrVIGCbMu",
|
||||||
|
"vNMojjUN76jc",
|
||||||
|
"6DG1au6rgOTl"
|
||||||
|
],
|
||||||
|
"title": "sessions",
|
||||||
|
"notePosition": 66,
|
||||||
|
"prefix": null,
|
||||||
|
"isExpanded": false,
|
||||||
|
"type": "text",
|
||||||
|
"mime": "text/html",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"name": "iconClass",
|
||||||
|
"value": "bx bx-table",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"name": "shareAlias",
|
||||||
|
"value": "sessions",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 30
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": "markdown",
|
||||||
|
"dataFileName": "sessions.md",
|
||||||
|
"attachments": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"isClone": false,
|
||||||
|
"noteId": "zWY2LKmas9os",
|
||||||
|
"notePath": [
|
||||||
|
"jdjRLhLV3TtI",
|
||||||
|
"MhwWMgxwDTZL",
|
||||||
|
"pRZhrVIGCbMu",
|
||||||
|
"vNMojjUN76jc",
|
||||||
|
"zWY2LKmas9os"
|
||||||
|
],
|
||||||
|
"title": "user_data",
|
||||||
|
"notePosition": 76,
|
||||||
|
"prefix": null,
|
||||||
|
"isExpanded": false,
|
||||||
|
"type": "text",
|
||||||
|
"mime": "text/html",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"name": "iconClass",
|
||||||
|
"value": "bx bx-table",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": "markdown",
|
||||||
|
"dataFileName": "user_data.md",
|
||||||
|
"attachments": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
| `isProtected` | Integer | Non-null | 0 | `1` if the entity is [protected](../../../Concepts/Protected%20entities.md), `0` otherwise. |
|
||||||
| `type` | Text | Non-null | `"text"` | The type of note (i.e. `text`, `file`, `code`, `relationMap`, `mermaid`, `canvas`). |
|
| `type` | Text | Non-null | `"text"` | The type of note (i.e. `text`, `file`, `code`, `relationMap`, `mermaid`, `canvas`). |
|
||||||
| `mime` | Text | Non-null | `"text/html"` | The MIME type of the note (e.g. `text/html`).. Note that it can be an empty string in some circumstances, but not null. |
|
| `mime` | Text | Non-null | `"text/html"` | The MIME type of the note (e.g. `text/html`).. Note that it can be an empty string in some circumstances, but not null. |
|
||||||
|
| `blobId` | Text | Nullable | `null` | The corresponding ID from <a class="reference-link" href="blobs.md">blobs</a>. Although it can theoretically be `NULL`, haven't found any such note yet. |
|
||||||
| `isDeleted` | Integer | Nullable | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
| `isDeleted` | Integer | Nullable | 0 | `1` if the entity is [deleted](../../../Concepts/Deleted%20notes.md), `0` otherwise. |
|
||||||
| `deleteId` | Text | Non-null | `null` | |
|
| `deleteId` | Text | Non-null | `null` | |
|
||||||
| `dateCreated` | Text | Non-null | | Localized creation date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
| `dateCreated` | Text | Non-null | | Localized creation date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||||
| `dateModified` | Text | Non-null | | Localized modification date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
| `dateModified` | Text | Non-null | | Localized modification date (e.g. `2023-11-08 18:43:44.204+0200`) |
|
||||||
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
| `utcDateCreated` | Text | Non-null | | Creation date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||||
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
| `utcDateModified` | Text | Non-null | | Modification date in UTC format (e.g. `2023-11-08 16:43:44.204Z`) |
|
||||||
| `blobId` | Text | Nullable | `null` | The corresponding ID from <a class="reference-link" href="blobs.md">blobs</a>. Although it can theoretically be `NULL`, haven't found any such note yet. |
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# revisions
|
# revisions
|
||||||
| Column Name | Data Type | Nullity | Default value | Description |
|
| Column Name | Data Type | Nullity | Default value | Description |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `revisionId` | TextText | Non-null | | Unique ID of the revision (e.g. `0GjgUqnEudI8`). |
|
| `revisionId` | Text | Non-null | | Unique ID of the revision (e.g. `0GjgUqnEudI8`). |
|
||||||
| `noteId` | Text | Non-null | | ID of the [note](notes.md) this revision belongs to. |
|
| `noteId` | Text | Non-null | | ID of the [note](notes.md) this revision belongs to. |
|
||||||
| `type` | Text | Non-null | `""` | The type of note (i.e. `text`, `file`, `code`, `relationMap`, `mermaid`, `canvas`). |
|
| `type` | Text | Non-null | `""` | The type of note (i.e. `text`, `file`, `code`, `relationMap`, `mermaid`, `canvas`). |
|
||||||
| `mime` | Text | Non-null | `""` | The MIME type of the note (e.g. `text/html`). |
|
| `mime` | Text | Non-null | `""` | The MIME type of the note (e.g. `text/html`). |
|
||||||
|
|||||||
8
docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/sessions.md
vendored
Normal file
8
docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/sessions.md
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# sessions
|
||||||
|
Contains user sessions for authentication purposes. The table is almost a direct mapping of the information that `express-session` requires.
|
||||||
|
|
||||||
|
| Column Name | Data Type | Nullity | Default value | Description |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `id` | Text | Non-null | | Unique, non-sequential ID of the session, directly as indicated by `express-session` |
|
||||||
|
| `data` | Text | Non-null | | The session information, in stringified JSON format. |
|
||||||
|
| `expires` | Integer | Non-null | | The expiration date of the session, extracted from the session information. Used to rapidly clean up expired sessions. |
|
||||||
17
docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/user_data.md
vendored
Normal file
17
docs/Developer Guide/Developer Guide/Architecture/Database/Database structure/user_data.md
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# user_data
|
||||||
|
Contains the user information for two-factor authentication. This table is **not** used for multi-user.
|
||||||
|
|
||||||
|
Relevant files:
|
||||||
|
|
||||||
|
* `apps/server/src/services/encryption/open_id_encryption.ts`
|
||||||
|
|
||||||
|
| Column Name | Data Type | Nullity | Default value | Description |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `tmpID` | Integer | | | A sequential ID of the user. Since only one user is supported by Trilium, this value is always zero. |
|
||||||
|
| `username` | Text | | | The user name as returned from the OAuth operation. |
|
||||||
|
| `email` | Text | | | The email as returned from the OAuth operation. |
|
||||||
|
| `userIDEncryptedDataKey` | Text | | | An encrypted hash of the user subject identifier from the OAuth operation. |
|
||||||
|
| `userIDVerificationHash` | Text | | | A salted hash of the subject identifier from the OAuth operation. |
|
||||||
|
| `salt` | Text | | | The verification salt. |
|
||||||
|
| `derivedKey` | Text | | | A random secure token. |
|
||||||
|
| `isSetup` | Text | | `"false"` | Indicates that the user has been saved (`"true"`). |
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
# Note Revisions
|
||||||
|
The revision API on the server side is managed by `apps/server/src/routes/api/revisions.ts`
|
||||||
@ -1,5 +1,5 @@
|
|||||||
# Documentation
|
# Documentation
|
||||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/6iUHD6bZIVmd/Documentation_image.png" width="205" height="162">
|
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/kaFXA5t813qK/Documentation_image.png" width="205" height="162">
|
||||||
|
|
||||||
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
|
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
|
||||||
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.
|
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.
|
||||||
|
|||||||
102
docs/User Guide/!!!meta.json
vendored
102
docs/User Guide/!!!meta.json
vendored
@ -12093,7 +12093,7 @@
|
|||||||
"R9pX4DGra2Vt",
|
"R9pX4DGra2Vt",
|
||||||
"ycBFjKrrwE9p"
|
"ycBFjKrrwE9p"
|
||||||
],
|
],
|
||||||
"title": "Exporting HTML for web publishing",
|
"title": "Exporting static HTML for web publishing",
|
||||||
"notePosition": 20,
|
"notePosition": 20,
|
||||||
"prefix": null,
|
"prefix": null,
|
||||||
"isExpanded": false,
|
"isExpanded": false,
|
||||||
@ -12130,7 +12130,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"dataFileName": "Exporting HTML for web publish.md",
|
"dataFileName": "Exporting static HTML for web .md",
|
||||||
"attachments": []
|
"attachments": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -14166,6 +14166,48 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"mime": "text/html",
|
"mime": "text/html",
|
||||||
"attributes": [
|
"attributes": [
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "Gzjqa934BdH4",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "wy8So3yZZlH9",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "R9pX4DGra2Vt",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "eIg8jdvaoNNd",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "GTwFsgaA0lCt",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "KSZ04uQ2D1St",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 60
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "label",
|
"type": "label",
|
||||||
"name": "iconClass",
|
"name": "iconClass",
|
||||||
@ -14179,48 +14221,6 @@
|
|||||||
"value": "read-only-db",
|
"value": "read-only-db",
|
||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 40
|
"position": 40
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "wy8So3yZZlH9",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 50
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "R9pX4DGra2Vt",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 60
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "Gzjqa934BdH4",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 70
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "eIg8jdvaoNNd",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "GTwFsgaA0lCt",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 90
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "KSZ04uQ2D1St",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 100
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
@ -14250,6 +14250,13 @@
|
|||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 10
|
"position": 10
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "relation",
|
||||||
|
"name": "internalLink",
|
||||||
|
"value": "xYmIYSP6wE3F",
|
||||||
|
"isInheritable": false,
|
||||||
|
"position": 20
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "label",
|
"type": "label",
|
||||||
"name": "shareAlias",
|
"name": "shareAlias",
|
||||||
@ -14263,13 +14270,6 @@
|
|||||||
"value": "bx bx-bot",
|
"value": "bx bx-bot",
|
||||||
"isInheritable": false,
|
"isInheritable": false,
|
||||||
"position": 30
|
"position": 30
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "relation",
|
|
||||||
"name": "internalLink",
|
|
||||||
"value": "xYmIYSP6wE3F",
|
|
||||||
"isInheritable": false,
|
|
||||||
"position": 40
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
|
|||||||
@ -50,7 +50,7 @@ You can view a list of all shared notes by clicking on "Show Shared Notes Subtre
|
|||||||
|
|
||||||
* Shared notes are published on the open internet and can be accessed by anyone with the URL unless the notes are password-protected.
|
* Shared notes are published on the open internet and can be accessed by anyone with the URL unless the notes are password-protected.
|
||||||
* The URL's randomness does not provide security, so it is crucial not to share sensitive information through this feature.
|
* The URL's randomness does not provide security, so it is crucial not to share sensitive information through this feature.
|
||||||
* Trilium takes precautions to protect your publicly shared instance from leaking information for non-shared notes, including opening a separate read-only connection to the <a class="reference-link" href="Database.md">Database</a>. Depending on your threat model, it might make more sense to use <a class="reference-link" href="Sharing/Exporting%20HTML%20for%20web%20publish.md">Exporting HTML for web publishing</a> and use battle-tested web servers such as Nginx or Apache to serve static content.
|
* Trilium takes precautions to protect your publicly shared instance from leaking information for non-shared notes, including opening a separate read-only connection to the <a class="reference-link" href="Database.md">Database</a>. Depending on your threat model, it might make more sense to use <a class="reference-link" href="Sharing/Exporting%20static%20HTML%20for%20web%20.md">Exporting HTML for web publishing</a> and use battle-tested web servers such as Nginx or Apache to serve static content.
|
||||||
|
|
||||||
### Password protection
|
### Password protection
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Exporting HTML for web publishing
|
# Exporting static HTML for web publishing
|
||||||
As described in <a class="reference-link" href="../Sharing.md">Sharing</a>, Trilium can act as a public server in which the shared notes are displayed in read-only mode. While this can work in most cases, it's generally not meant for high-traffic websites and since it's running on a Node.js server it can be potentially exploited.
|
As described in <a class="reference-link" href="../Sharing.md">Sharing</a>, Trilium can act as a public server in which the shared notes are displayed in read-only mode. While this can work in most cases, it's generally not meant for high-traffic websites and since it's running on a Node.js server it can be potentially exploited.
|
||||||
|
|
||||||
Another alternative is to generate static HTML files (just like other static site generators such as [MkDocs](https://www.mkdocs.org/)). Since the normal HTML ZIP export does not contain any styling or additional functionality, Trilium provides a way to export the same layout and style as the <a class="reference-link" href="../Sharing.md">Sharing</a> function into static HTML files.
|
Another alternative is to generate static HTML files (just like other static site generators such as [MkDocs](https://www.mkdocs.org/)). Since the normal HTML ZIP export does not contain any styling or additional functionality, Trilium provides a way to export the same layout and style as the <a class="reference-link" href="../Sharing.md">Sharing</a> function into static HTML files.
|
||||||
@ -23,6 +23,10 @@ Apart from normal <a class="reference-link" href="../Sharing.md">Sharing</a>, e
|
|||||||
|
|
||||||
* The name of the files/URLs will prefer `shareAlias` to allow for clean URLs.
|
* The name of the files/URLs will prefer `shareAlias` to allow for clean URLs.
|
||||||
* The export requires a functional web server as the pages will not render properly if accessed locally via a web browser due to the use of module scripts.
|
* The export requires a functional web server as the pages will not render properly if accessed locally via a web browser due to the use of module scripts.
|
||||||
|
* The directory structure is also slightly different:
|
||||||
|
* A normal HTML export results in an index file and a single directory.
|
||||||
|
* Instead, for static exporting the top-root level becomes the index file and the child directories are on the root instead.
|
||||||
|
* This makes it possible to easily publish to a website, without forcing everything but the root note to be in a sub-directory.
|
||||||
|
|
||||||
## Testing locally
|
## Testing locally
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ class MyWidget extends api.BasicWidget {
|
|||||||
get parentWidget() { return "left-pane"; }
|
get parentWidget() { return "left-pane"; }
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $("");
|
this.$widget = $("<div id='my-widget'>");
|
||||||
return this.$widget;
|
return this.$widget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -25,14 +25,14 @@ To implement this widget:
|
|||||||
2. Assign the `#widget` [attribute](../../../Advanced%20Usage/Attributes.md) to the [note](../../../Basic%20Concepts%20and%20Features/Notes.md).
|
2. Assign the `#widget` [attribute](../../../Advanced%20Usage/Attributes.md) to the [note](../../../Basic%20Concepts%20and%20Features/Notes.md).
|
||||||
3. Restart Trilium or reload the window.
|
3. Restart Trilium or reload the window.
|
||||||
|
|
||||||
To verify that the widget is working, open the developer tools (`Cmd` + `Shift` + `I`) and run `document.querySelector("#my-widget")`. If the element is found, the widget is functioning correctly. If `undefined` is returned, double-check that the [note](../../../Basic%20Concepts%20and%20Features/Notes.md) has the `#widget` [attribute](../../../Advanced%20Usage/Attributes.md).
|
To verify that the widget is working, open the developer tools (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>) and run `document.querySelector("#my-widget")`. If the element is found, the widget is functioning correctly. If `undefined` is returned, double-check that the [note](../../../Basic%20Concepts%20and%20Features/Notes.md) has the `#widget` [attribute](../../../Advanced%20Usage/Attributes.md).
|
||||||
|
|
||||||
### Step 2: Adding an UI Element
|
### Step 2: Adding an UI Element
|
||||||
|
|
||||||
Next, let's improve the widget by adding a button to it.
|
Next, let's improve the widget by adding a button to it.
|
||||||
|
|
||||||
```
|
```
|
||||||
const template = ``;
|
const template = `<div id="my-widget"><button>Click Me!</button></div>`;
|
||||||
|
|
||||||
class MyWidget extends api.BasicWidget {
|
class MyWidget extends api.BasicWidget {
|
||||||
get position() {return 1;}
|
get position() {return 1;}
|
||||||
@ -56,7 +56,7 @@ To make the button more visually appealing and position it correctly, we'll appl
|
|||||||
Here's the updated template:
|
Here's the updated template:
|
||||||
|
|
||||||
```
|
```
|
||||||
const template = ``;
|
const template = `<div id="my-widget"><button class="tree-floating-button bx bxs-magic-wand tree-settings-button"></button></div>`;
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, we'll adjust the button's position using CSS:
|
Next, we'll adjust the button's position using CSS:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user