Compare commits

..

23 Commits

Author SHA1 Message Date
perf3ct
5f1773609f fix(tests): rename some of the silly-ily named tests 2025-11-04 15:56:49 -08:00
perf3ct
da0302066d fix(tests): resolve issues with new search tests not passing 2025-11-04 15:55:42 -08:00
perf3ct
942647ab9c fix(search): get rid of exporting dbConnection 2025-11-04 14:47:46 -08:00
perf3ct
b8aa7402d8 feat(tests): create a ton of tests for the various search capabilities that we support 2025-11-04 14:34:50 -08:00
perf3ct
052e28ab1b feat(search): if the search is empty, return all notes 2025-11-04 11:59:41 -08:00
perf3ct
16912e606e fix(search): resolve compilation issue due to performance log in new search 2025-11-03 12:04:00 -08:00
Jon Fuller
321752ac18 Merge branch 'main' into feat/rice-searching-with-sqlite 2025-11-03 11:47:44 -08:00
perf3ct
10988095c2 feat(search): get the correct comparison and rice out the fts5 search 2025-10-27 14:37:44 -07:00
perf3ct
253da139de feat(search): try again to get fts5 searching done well 2025-10-24 21:47:06 -07:00
Jon Fuller
d992a5e4a2 Merge branch 'main' into feat/rice-searching-with-sqlite 2025-10-24 09:18:11 -07:00
perf3ct
58c225237c feat(search): try a ground-up sqlite search approach 2025-09-03 00:34:55 +00:00
perf3ct
d074841885 Revert "feat(search): try to get fts search to work in large environments"
This reverts commit 053f722cb8.
2025-09-02 19:24:50 +00:00
perf3ct
06b2d71b27 Revert "feat(search): try to decrease complexity"
This reverts commit 5b79e0d71e.
2025-09-02 19:24:47 +00:00
perf3ct
0afb8a11c8 Revert "feat(search): try to deal with huge dbs, might need to squash later"
This reverts commit 37d0136c50.
2025-09-02 19:24:46 +00:00
perf3ct
f529ddc601 Revert "feat(search): further improve fts search"
This reverts commit 7c5553bd4b.
2025-09-02 19:24:45 +00:00
perf3ct
8572f82e0a Revert "feat(search): I honestly have no idea what I'm doing"
This reverts commit b09a2c386d.
2025-09-02 19:24:44 +00:00
perf3ct
b09a2c386d feat(search): I honestly have no idea what I'm doing 2025-09-01 22:29:59 -07:00
perf3ct
7c5553bd4b feat(search): further improve fts search 2025-09-01 21:40:05 -07:00
perf3ct
37d0136c50 feat(search): try to deal with huge dbs, might need to squash later 2025-09-01 04:33:10 +00:00
perf3ct
5b79e0d71e feat(search): try to decrease complexity 2025-08-30 22:30:01 -07:00
perf3ct
053f722cb8 feat(search): try to get fts search to work in large environments 2025-08-31 03:15:29 +00:00
perf3ct
21aaec2c38 feat(search): also fix tests for new fts functionality 2025-08-30 20:48:42 +00:00
perf3ct
1db4971da6 feat(search): implement FST5 w/ sqlite for faster and better searching
feat(search): don't limit the number of blobs to put in virtual tables

fix(search): improve FTS triggers to handle all SQL operations correctly

The root cause of FTS index issues during import was that database triggers
weren't properly handling all SQL operations, particularly upsert operations
(INSERT ... ON CONFLICT ... DO UPDATE) that are commonly used during imports.

Key improvements:
- Fixed INSERT trigger to handle INSERT OR REPLACE operations
- Updated UPDATE trigger to fire on ANY change (not just specific columns)
- Improved blob triggers to use INSERT OR REPLACE for atomic updates
- Added proper handling for notes created before their blobs (import scenario)
- Added triggers for protection state changes
- All triggers now use LEFT JOIN to handle missing blobs gracefully

This ensures the FTS index stays synchronized even when:
- Entity events are disabled during import
- Notes are re-imported (upsert operations)
- Blobs are deduplicated across notes
- Notes are created before their content blobs

The solution works entirely at the database level through triggers,
removing the need for application-level workarounds.

fix(search): consolidate FTS trigger fixes into migration 234

- Merged improved trigger logic from migration 235 into 234
- Deleted unnecessary migration 235 since DB version is still 234
- Ensures triggers handle all SQL operations (INSERT OR REPLACE, upserts)
- Fixes FTS indexing for imported notes by handling missing blobs
- Schema.sql and migration 234 now have identical trigger implementations
2025-08-30 20:39:40 +00:00
392 changed files with 21526 additions and 10484 deletions

View File

@@ -77,7 +77,7 @@ jobs:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Publish release
uses: softprops/action-gh-release@v2.4.2
uses: softprops/action-gh-release@v2.4.1
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false
@@ -118,7 +118,7 @@ jobs:
arch: ${{ matrix.arch }}
- name: Publish release
uses: softprops/action-gh-release@v2.4.2
uses: softprops/action-gh-release@v2.4.1
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- hotfix
paths-ignore:
- "apps/website/**"
pull_request:
@@ -14,24 +13,8 @@ permissions:
contents: read
jobs:
e2e:
strategy:
fail-fast: false
matrix:
include:
- name: linux-x64
os: ubuntu-22.04
arch: x64
- name: linux-arm64
os: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.os }}
name: E2E tests on ${{ matrix.name }}
env:
TRILIUM_DOCKER: 1
TRILIUM_PORT: 8082
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
TRILIUM_INTEGRATION_TEST: memory
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
@@ -46,34 +29,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps
- name: Build the server
uses: ./.github/actions/build-server
with:
os: linux
arch: ${{ matrix.arch }}
- name: Unpack and start the server
run: |
version=$(node --eval "console.log(require('./package.json').version)")
file=$(find ./upload -name '*.tar.xz' -print -quit)
name=$(basename "$file" .tar.xz)
mkdir -p ./server-dist
tar -xvf "$file" -C ./server-dist
server_dir="./server-dist/TriliumNotes-Server-$version-linux-${{ matrix.arch }}"
if [ ! -d "$server_dir" ]; then
echo Missing dir.
exit 1
fi
cd "$server_dir"
"./trilium.sh" &
sleep 10
- name: Server end-to-end tests
run: pnpm --filter server-e2e e2e
- run: pnpm --filter server-e2e e2e
- name: Upload test report
if: failure()
@@ -81,7 +39,3 @@ jobs:
with:
name: e2e report
path: apps/server-e2e/test-output
- name: Kill the server
if: always()
run: pkill -f trilium || true

View File

@@ -127,7 +127,7 @@ jobs:
path: upload
- name: Publish stable release
uses: softprops/action-gh-release@v2.4.2
uses: softprops/action-gh-release@v2.4.1
with:
draft: false
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md

View File

@@ -41,12 +41,12 @@
"@types/node": "24.10.0",
"@types/yargs": "17.0.34",
"@vitest/coverage-v8": "3.2.4",
"eslint": "9.39.1",
"eslint": "9.39.0",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",
"jsdoc": "4.0.5",
"lorem-ipsum": "2.0.8",
"rcedit": "5.0.0",
"rcedit": "4.0.1",
"rimraf": "6.1.0",
"tslib": "2.8.1"
},

View File

@@ -11,7 +11,7 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.20.0",
"devDependencies": {
"@redocly/cli": "2.11.0",
"@redocly/cli": "2.10.0",
"archiver": "7.0.1",
"fs-extra": "11.3.2",
"react": "19.2.0",

View File

@@ -22,9 +22,8 @@ async function main() {
buildSwagger(context);
buildScriptApi(context);
// Copy index and 404 files.
// Copy index file.
cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html"));
cpSync(join(context.baseDir, "user-guide/404.html"), join(context.baseDir, "404.html"));
}
main();

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.99.4",
"version": "0.99.3",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -15,7 +15,7 @@
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
},
"dependencies": {
"@eslint/js": "9.39.1",
"@eslint/js": "9.39.0",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
@@ -43,7 +43,7 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"i18next": "25.6.1",
"i18next": "25.6.0",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
@@ -53,13 +53,13 @@
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "16.4.2",
"marked": "16.4.1",
"mermaid": "11.12.1",
"mind-elixir": "5.3.5",
"mind-elixir": "5.3.4",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.27.2",
"react-i18next": "16.2.4",
"react-i18next": "16.2.3",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",

View File

@@ -499,10 +499,6 @@ type EventMappings = {
noteIds: string[];
};
refreshData: { ntxId: string | null | undefined };
contentSafeMarginChanged: {
top: number;
noteContext: NoteContext;
}
};
export type EventListener<T extends EventNames> = {

View File

@@ -7,6 +7,7 @@ import protectedSessionService from "../services/protected_session.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
import LlmChatPanel from "../widgets/llm_chat_panel.js";
import toastService from "../services/toast.js";
import noteCreateService from "../services/note_create.js";
@@ -170,8 +171,7 @@ export default class RootCommandExecutor extends Component {
}
toggleTrayCommand() {
if (!utils.isElectron() || options.is("disableTray")) return;
if (!utils.isElectron()) return;
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
const isVisible = windows.every((w) => w.isVisible());

View File

@@ -1,49 +1,47 @@
import { applyModals } from "./layout_commons.js";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import ApiLog from "../widgets/api_log.jsx";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import ContentHeader from "../widgets/containers/content-header.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import FindWidget from "../widgets/find.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import options from "../services/options.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import SpacerWidget from "../widgets/spacer.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
import TabRowWidget from "../widgets/tab_row.js";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteDetailWidget from "../widgets/note_detail.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteIconWidget from "../widgets/note_icon.jsx";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import SpacerWidget from "../widgets/spacer.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import FindWidget from "../widgets/find.js";
import TocWidget from "../widgets/toc.js";
import HighlightsListWidget from "../widgets/highlights_list.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import { applyModals } from "./layout_commons.js";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import SearchResult from "../widgets/search_result.jsx";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import ApiLog from "../widgets/api_log.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
export default class DesktopLayout {
@@ -131,15 +129,12 @@ export default class DesktopLayout {
.child(<CreatePaneButton />)
)
.child(<Ribbon />)
.child(<SharedInfo />)
.child(new WatchedFileUpdateStatusWidget())
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
.child(
new ScrollingContainer()
.filling()
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />)
)
.child(new PromotedAttributesWidget())
.child(<SqlTableSchemas />)
.child(new NoteDetailWidget())

View File

@@ -1,34 +1,32 @@
import { applyModals } from "./layout_commons.js";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import ContentHeader from "../widgets/containers/content-header.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import RootContainer from "../widgets/containers/root_container.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import type AppContext from "../components/app_context.js";
import TabRowWidget from "../widgets/tab_row.js";
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
import { applyModals } from "./layout_commons.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfoWidget from "../widgets/shared_info.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import TabRowWidget from "../widgets/tab_row.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import type AppContext from "../components/app_context.js";
const MOBILE_CSS = `
<style>
@@ -151,16 +149,13 @@ export default class MobileLayout {
.child(<NoteTitleWidget />)
.child(<MobileDetailMenu />)
)
.child(<SharedInfoWidget />)
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
.child(new PromotedAttributesWidget())
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfoWidget />)
)
.child(new NoteDetailWidget())
.child(<NoteList media="screen" />)
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)

View File

@@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
command: "editBranchPrefix",
keyboardShortcut: "editBranchPrefix",
uiIcon: "bx bx-rename",
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
},
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },

View File

@@ -1,5 +1,3 @@
@import "boxicons/css/boxicons.min.css";
:root {
--print-font-size: 11pt;
--ck-content-color-image-caption-background: transparent !important;

View File

@@ -70,9 +70,6 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
});
})
);
// Check custom CSS.
await loadCustomCss(note);
}
load().then(() => requestAnimationFrame(onReady))
@@ -92,10 +89,7 @@ function CollectionRenderer({ note, onReady }: RendererProps) {
ntxId="print"
highlightedTokens={null}
media="print"
onReady={async () => {
await loadCustomCss(note);
onReady();
}}
onReady={onReady}
/>;
}
@@ -108,25 +102,4 @@ function Error404({ noteId }: { noteId: string }) {
)
}
async function loadCustomCss(note: FNote) {
const printCssNotes = await note.getRelationTargets("printCss");
let loadPromises: JQueryPromise<void>[] = [];
for (const printCssNote of printCssNotes) {
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
const linkEl = document.createElement("link");
linkEl.href = `/api/notes/${printCssNote.noteId}/download`;
linkEl.rel = "stylesheet";
const promise = $.Deferred();
loadPromises.push(promise.promise());
linkEl.onload = () => promise.resolve();
document.head.appendChild(linkEl);
}
await Promise.allSettled(loadPromises);
}
main();

View File

@@ -159,7 +159,7 @@ describe("shortcuts", () => {
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
// Special keys
for (const keyCode of [ "Delete", "Enter", "NumpadEnter" ]) {
for (const keyCode of [ "Delete", "Enter" ]) {
event = createKeyboardEvent({ key: keyCode, code: keyCode });
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
}

View File

@@ -46,7 +46,6 @@ for (let i = 1; i <= 19; i++) {
const KEYCODES_WITH_NO_MODIFIER = new Set([
"Delete",
"Enter",
"NumpadEnter",
...functionKeyCodes
]);

View File

@@ -24,9 +24,7 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
continue;
}
if (glob.device !== "print") {
applyCopyToClipboardButton($(codeBlock));
}
if (syntaxHighlightingEnabled) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);

View File

@@ -841,7 +841,7 @@ export function arrayEqual<T>(a: T[], b: T[]) {
return true;
}
export type Indexed<T extends object> = T & { index: number };
type Indexed<T extends object> = T & { index: number };
/**
* Given an object array, alters every object in the array to have an index field assigned to it.

View File

@@ -5,6 +5,7 @@
.note-detail-relation-map {
height: 100%;
overflow: hidden !important;
padding: 10px;
position: relative;
}

View File

@@ -1104,6 +1104,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
.card {
color: inherit !important;
background-color: inherit !important;
border-color: var(--main-border-color) !important;
}
@@ -1758,10 +1759,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
flex-direction: column;
margin-inline-start: 10px;
margin-inline-end: 5px;
background: transparent;
}
#right-pane .card-header {
background: inherit;
padding: 6px 0 3px 0;
width: 99%; /* to give minimal right margin */
background-color: var(--button-background-color);
@@ -1808,15 +1809,12 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
}
.note-split {
/* Limits the maximum width of the note */
--max-content-width: var(--preferred-max-content-width);
margin-inline-start: auto;
margin-inline-end: auto;
}
.note-split.full-content-width {
--max-content-width: unset;
max-width: 999999px;
}
button.close:hover {
@@ -2036,16 +2034,13 @@ body.zen #right-pane,
body.zen #mobile-sidebar-wrapper,
body.zen .tab-row-container,
body.zen .tab-row-widget,
body.zen .shared-info-widget,
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
body.zen .note-icon-widget,
body.zen .title-row .icon-action,
body.zen .promoted-attributes-widget,
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
body.zen .action-button,
body.zen .note-split:not(.type-book) .note-list-widget {
body.zen .action-button {
display: none !important;
}
@@ -2089,121 +2084,12 @@ body.zen .note-title-widget,
body.zen .note-title-widget input {
font-size: 1rem !important;
background: transparent !important;
pointer-events: none;
}
body.zen #detail-container {
width: 100%;
}
body.zen .note-split:not(.full-content-width) .scrolling-container {
display: flex;
flex-direction: column;
scroll-behavior: unset !important;
}
body.zen .note-split:not(.full-content-width) .note-detail {
margin: auto;
padding-bottom: 25vh;
max-width: var(--max-content-width);
width: 100%;
}
body.zen .note-split:not(.full-content-width) .scroll-padding-widget {
display: none;
}
body.zen .note-split.type-text {
position: relative;
font-size: 1.15em;
}
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .title-row {
--start-color: var(--main-background-color);
position: absolute;
width: 100%;
background: linear-gradient(var(--start-color) 30%, transparent 100%);
z-index: 1000;
}
@supports (background: color-mix(in srgb, white, transparent)) {
body.zen .note-split.type-text .title-row {
--start-color: color-mix(in srgb, var(--main-background-color), transparent 10%);
}
}
body.zen .note-split.type-text .scrolling-container {
--padding-bottom: 130px; /* Should be enough to avoid caret being hidden by the formatting toolbar */
/* (Usually) keeps the caret above the fixed toolbar */
scroll-padding-bottom: var(--padding-bottom);
}
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
--padding-top: 50px; /* Should be enough to cover the title row */
padding-top: var(--padding-top);
scroll-padding-top: var(--padding-top);
}
/* Fixed formatting toolbar */
body.zen .note-split .ribbon-container {
position: fixed;
left: 0;
bottom: 20px;
width: 100%;
z-index: 1000;
opacity: 0; /* Hidden unless the current note split is focused */
pointer-events: none;
transition: opacity 100ms linear;
}
body.zen .note-split:focus-within .ribbon-container {
opacity: 1; /* Show when the note split is focused */
}
body.zen .note-split .ribbon-container .ribbon-body {
border: 0;
}
body.zen .note-split .ribbon-container .classic-toolbar-widget {
margin: auto;
width: fit-content;
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
border-radius: 8px;
border: 1px solid var(--main-border-color);
padding: 4px;
background: var(--menu-background-color);
}
body.zen .note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
/* Hide the toolbar wrapper if the toolbar is missing */
display: none;
}
body.zen .note-split:focus-within .ribbon-container .classic-toolbar-widget {
pointer-events: all;
}
@media (max-width: 1300px) {
body.zen .note-split .ribbon-container .classic-toolbar-widget {
/* Set the toolbar to full with */
width: 100%;
}
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
/* Force toolbar items overflow dropdowns open upwards */
top: auto;
bottom: 100%;
}
}
/* Content renderer */
footer.file-footer,
@@ -2520,7 +2406,7 @@ footer.webview-footer button {
transform: rotate(180deg);
}
/* CK Editor */
/* CK Edito */
/* Insert text snippet: limit the width of the listed items to avoid overly long names */
:root body.desktop div.ck-template-form li.ck-list__item .ck-template-form__text-part > span {
@@ -2551,17 +2437,3 @@ iframe.print-iframe {
.excalidraw.theme--dark canvas {
--theme-filter: invert(100%) hue-rotate(180deg);
}
/* Scrolling container */
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)) {
display: flex;
flex-direction: column;
}
.scrolling-container > .note-detail.full-height,
.scrolling-container > .note-list-widget.full-height {
position: relative;
flex-grow: 1;
width: 100%;
}

View File

@@ -15,7 +15,7 @@
--native-titlebar-background: #00000000;
--window-background-color-bgfx: transparent; /* When background effects enabled */
--main-background-color: #242424;
--main-background-color: #272727;
--main-text-color: #ccc;
--main-border-color: #454545;
--subtle-border-color: #313131;
@@ -166,9 +166,6 @@
--protected-session-active-icon-color: #8edd8e;
--sync-status-error-pulse-color: #f47871;
--center-pane-vert-layout-background-color-bgfx: #0c0c0c69;
--center-pane-horiz-layout-background-color-bgfx: #1e1e1ec7;
--right-pane-heading-color: gray;
--root-background: var(--left-pane-background-color);
@@ -195,8 +192,8 @@
--badge-background-color: #ffffff1a;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: #ffffff21;
--promoted-attribute-card-shadow: none;
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #000000b3;
--floating-button-shadow-color: #00000080;
--floating-button-background-color: #494949d2;
@@ -211,8 +208,6 @@
--floating-button-hide-button-background: #00000029;
--floating-button-hide-button-color: #ffffff63;
--right-pane-background-color: var(--main-background-color);
--right-pane-background-color-bgfx: #0c0c0c24; /* Only for the vertical layout */
--right-pane-item-hover-background: #ffffff26;
--right-pane-item-hover-color: white;
@@ -230,9 +225,10 @@
--code-block-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
--card-background-color: #ffffff12;
--card-background-hover-color: #ffffff20;
--card-border-color: transparent;
--card-box-shadow: none;
--card-background-hover-color: #3c3c3c;
--card-background-press-color: #464646;
--card-border-color: #222222;
--card-box-shadow: 0 0 12px rgba(0, 0, 0, 0.15);
--calendar-color: var(--menu-text-color);
--calendar-weekday-labels-color: var(--muted-text-color);
@@ -299,9 +295,3 @@ body ::-webkit-calendar-picker-indicator {
body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important;
}
.tinted-quick-edit-dialog {
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
}

View File

@@ -159,9 +159,6 @@
--protected-session-active-icon-color: #16b516;
--sync-status-error-pulse-color: #ff5528;
--center-pane-vert-layout-background-color-bgfx: #ffffff75;
--center-pane-horiz-layout-background-color-bgfx: #ffffffd6;
--right-pane-heading-color: gray;
--root-background: var(--left-pane-background-color);
@@ -183,13 +180,13 @@
--inactive-tab-hover-background-color: #00000016;
--inactive-tab-text-color: #4e4e4e;
--alert-bar-background: #f9cf2b29;
--alert-bar-background: #32637b29;
--badge-background-color: #00000011;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: #00000014;
--promoted-attribute-card-shadow: none;
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #00000033;
--floating-button-shadow-color: #00000042;
--floating-button-background-color: #eaeaeacc;
@@ -210,9 +207,7 @@
--new-tab-button-hover-background: white;
--new-tab-button-hover-color: black;
--right-pane-background-color: var(--main-background-color);
--right-pane-background-color-bgfx: #ffffff9e; /* Only for the vertical layout */
--right-pane-item-hover-background: #00000013;
--right-pane-item-hover-background: #ececec;
--right-pane-item-hover-color: inherit;
--scrollbar-thumb-color: #0000005c;
@@ -228,11 +223,12 @@
--code-block-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1), 0px 0px 2px rgba(0, 0, 0, 0.2);
--card-background-color: #0000000d;
--card-background-hover-color: #0000001c;
--card-border-color: transparent;
--card-background-color: var(--accented-background-color);
--card-background-hover-color: #f9f9f9;
--card-background-press-color: #efefef;
--card-border-color: #eaeaea;
--card-shadow-color: rgba(0, 0, 0, 0.1);
--card-box-shadow: none;
--card-box-shadow: 0 0 12px var(--card-shadow-color);
--calendar-color: var(--menu-text-color);
--calendar-weekday-labels-color: var(--muted-text-color);
@@ -275,9 +271,3 @@
* This value is unset for gray tones. */
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
}
.tinted-quick-edit-dialog {
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
}

View File

@@ -82,7 +82,6 @@
/* Theme capabilities */
--tab-note-icons: true;
--allow-background-effects: true;
/* To ensure that a tree item's custom color remains sufficiently contrasted and readable,
* the color is adjusted based on the current color scheme (light or dark). The lightness
@@ -132,8 +131,7 @@ body.mobile .dropdown-menu .dropdown-menu {
body.desktop .dropdown-menu::before,
:root .ck.ck-dropdown__panel::before,
:root .excalidraw .popover::before,
body.zen .note-split .ribbon-container .classic-toolbar-widget::before {
:root .excalidraw .popover::before {
content: "";
backdrop-filter: var(--dropdown-backdrop-filter);
border-radius: var(--dropdown-border-radius);
@@ -487,21 +485,13 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
--note-list-vertical-padding: 15px;
background-color: var(--card-background-color);
border: 1px solid var(--card-border-color) !important;
box-shadow: 2px 3px 4px var(--card-shadow-color);
border-radius: 12px;
user-select: none;
padding: 0;
margin: 5px 10px 5px 0;
}
:root .note-list .note-book-card:hover {
background-color: var(--card-background-hover-color);
transition: background-color 200ms ease-out;
}
:root .note-list.grid-view .note-book-card:active {
transform: scale(.98);
}
.note-list.list-view .note-book-card {
box-shadow: 0 0 3px var(--card-shadow-color);
}
@@ -510,6 +500,10 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
vertical-align: middle;
}
.note-list-wrapper .note-book-card:active {
background-color: var(--card-background-press-color);
}
.note-list-wrapper .note-book-card a {
color: inherit !important;
}
@@ -591,6 +585,7 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
}
.note-list.grid-view .note-book-card:hover {
background: var(--card-background-color) !important;
filter: contrast(105%);
}

View File

@@ -258,6 +258,11 @@
border-inline-start: 1px solid var(--ck-color-toolbar-border);
}
/* The last separator of the toolbar */
:root .classic-toolbar-widget .ck.ck-toolbar__separator:last-of-type {
flex-grow: 1;
}
/* Heading dropdown */
:root .ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__panel .ck-list__item {
@@ -675,16 +680,3 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
.ck-content a.reference-link > span {
text-decoration: underline;
}
/*
* Read-only text content
*/
.note-detail-readonly-text:focus-visible {
outline: 2px solid var(--input-focus-outline-color);
border-radius: 4px;
}
.note-list-widget {
outline: 0 !important;
}

View File

@@ -101,7 +101,7 @@
.sql-table-schemas-widget .sql-table-schemas button:hover,
.sql-table-schemas-widget .sql-table-schemas button:active,
.sql-table-schemas-widget .sql-table-schemas button:focus-visible {
--background: var(--card-background-hover-color);
--background: var(--card-background-press-color);
--color: var(--main-text-color);
}
@@ -123,12 +123,8 @@
*/
/* The container */
.note-split.empty-note {
--max-content-width: 70%;
}
.note-split.empty-note div.note-detail {
div.note-detail-empty {
max-width: 70%;
margin: 50px auto;
}
@@ -152,7 +148,7 @@
--options-card-min-width: 500px;
--options-card-max-width: 900px;
--options-card-padding: 17px;
--options-title-font-size: .75rem;
--options-title-font-size: 1rem;
--options-title-offset: 13px;
}
/* Create a gap at the top of the option pages */
@@ -177,19 +173,16 @@
}
.options-section:not(.tn-no-card) {
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
box-shadow: var(--card-box-shadow);
margin: auto;
border-radius: 12px;
border: 1px solid var(--card-border-color) !important;
border-radius: 8px;
box-shadow: var(--card-box-shadow);
background: var(--card-background-color);
padding: var(--options-card-padding);
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
}
body.prefers-centered-content .options-section:not(.tn-no-card) {
margin-inline: auto;
}
body.desktop .options-section:not(.tn-no-card) {
body.desktop .option-section:not(.tn-no-card) {
min-width: var(--options-card-min-width);
max-width: var(--options-card-max-width);
}
@@ -200,16 +193,9 @@ body.desktop .options-section:not(.tn-no-card) {
padding-bottom: var(--default-padding);
}
.options-section:not(.tn-no-card) h4,
.options-section:not(.tn-no-card) h5 {
text-transform: uppercase;
letter-spacing: .4pt;
}
.options-section:not(.tn-no-card) h4 {
font-size: var(--options-title-font-size);
font-weight: 600;
font-weight: bold;
color: var(--launcher-pane-text-color);
margin-top: calc(-1 * var(--options-card-padding) - var(--options-title-font-size) - var(--options-title-offset)) !important;
margin-bottom: calc(var(--options-title-offset) + var(--options-card-padding)) !important;

View File

@@ -34,7 +34,6 @@
div.promoted-attributes-container {
margin-top: 8px;
margin-bottom: 8px;
margin-inline-start: 12px;
}
/*

View File

@@ -8,7 +8,7 @@
}
:root {
--dropdown-backdrop-filter: blur(20px) saturate(6);
--dropdown-backdrop-filter: blur(10px) saturate(6);
--dropdown-border-radius: 10px;
}
@@ -35,53 +35,30 @@ body.mobile {
}
/* #region Mica */
body.background-effects.platform-win32 {
/* Quirk: --background-material is read before "theme-supports-background-effects" class
* is applied. Apply the matterial even if the theme doesn't support it. */
--background-material: tabbed;
}
body.background-effects.theme-supports-background-effects.platform-win32 {
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
--tab-background-color: var(--window-background-color-bgfx);
--new-tab-button-background: var(--window-background-color-bgfx);
--active-tab-background-color: var(--launcher-pane-horiz-background-color);
--root-background: transparent;
}
body.background-effects.platform-win32.layout-vertical {
--left-pane-background-color: var(--window-background-color-bgfx);
--background-material: mica;
}
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical {
--left-pane-background-color: var(--window-background-color-bgfx);
--center-pane-background-color-bgfx: var(--center-pane-vert-layout-background-color-bgfx);
--right-pane-background-color: var(--right-pane-background-color-bgfx);
}
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal {
--center-pane-background-color-bgfx: var(--center-pane-horiz-layout-background-color-bgfx);
--gutter-color: var(--left-pane-background-color);
}
body.background-effects.theme-supports-background-effects.platform-win32,
body.background-effects.theme-supports-background-effects.platform-win32 #root-widget {
body.background-effects.platform-win32,
body.background-effects.platform-win32 #root-widget {
background: var(--window-background-color-bgfx) !important;
}
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal #horizontal-main-container,
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical #vertical-main-container {
body.background-effects.platform-win32.layout-horizontal #horizontal-main-container,
body.background-effects.platform-win32.layout-vertical #vertical-main-container {
background-color: var(--root-background);
}
/* Note split with background effects */
body.background-effects.theme-supports-background-effects.platform-win32 #center-pane .note-split.bgfx {
--note-split-background-color: var(--center-pane-background-color-bgfx);
}
/* #endregion */
/* Matches when the left pane is collapsed */
@@ -95,21 +72,9 @@ body.layout-vertical #horizontal-main-container.left-pane-hidden #launcher-pane.
border-inline-end: 2px solid var(--left-pane-collapsed-border-color);
}
/*
* Zen mode
*/
@keyframes zen-formatting-toolbar-entrance {
from {
transform: translateY(200%);
} to {
transform: translateY(0);
}
}
body.zen .note-split .ribbon-container .classic-toolbar-widget {
position: relative;
animation: zen-formatting-toolbar-entrance 300ms ease-out;
body.background-effects.zen #root-widget {
--main-background-color: transparent;
--root-background: transparent;
}
/*
@@ -1206,18 +1171,23 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
* CENTER PANE
*/
/* The first visible note split */
.vertical-layout #center-pane .note-split:not(.visible ~ .visible) {
#center-pane {
background: var(--main-background-color);
}
.vertical-layout #center-pane {
border-radius: var(--center-pane-border-radius) 0 0 0;
}
#center-pane .note-split {
.note-split {
padding-top: 2px;
background-color: var(--note-split-background-color, var(--main-background-color));
animation: note-entrance 100ms linear;
/* will-change: opacity; -- causes some weird artifacts to the note menu in split view */
}
body:not(.background-effects) #center-pane .note-split {
animation: note-entrance 100ms linear;
.split-note-container-widget > .gutter {
background: var(--root-background) !important;
transition: background 150ms ease-out;
}
/*
@@ -1230,9 +1200,9 @@ body:not(.background-effects) #center-pane .note-split {
@keyframes note-entrance {
from {
filter: opacity(0);
opacity: 0;
} to {
filter: opacity(1);
opacity: 1;
}
}
@@ -1358,7 +1328,8 @@ div.promoted-attribute-cell {
--pa-card-padding-inline-end: 2px;
--input-background-color: transparent;
box-shadow: var(--promoted-attribute-card-shadow);
box-shadow: 1px 1px 2px var(--promoted-attribute-card-shadow-color);
display: inline-flex;
margin: 0;
border-radius: 8px;
@@ -1745,7 +1716,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
*/
#right-pane {
background: var(--right-pane-background-color);
background: var(--main-background-color);
}
#right-pane div.card-header {

View File

@@ -520,7 +520,9 @@
"max_content_width": {
"max_width_unit": "بكسل",
"title": "عرض المحتوى",
"max_width_label": "اقصى عرض للمحتوى"
"reload_button": "اعادة تحميل الواجهة",
"max_width_label": "اقصى عرض للمحتوى",
"reload_description": "تغييرات من خيارات المظهر"
},
"native_title_bar": {
"enabled": "مفعل",

View File

@@ -39,10 +39,7 @@
"help_on_tree_prefix": "有关树前缀的帮助",
"prefix": "前缀: ",
"save": "保存",
"branch_prefix_saved": "分支前缀已保存。",
"edit_branch_prefix_multiple": "编辑 {{count}} 个分支的前缀",
"branch_prefix_saved_multiple": "已为 {{count}} 个分支保存分支前缀。",
"affected_branches": "受影响的分支 {{count}}:"
"branch_prefix_saved": "分支前缀已保存。"
},
"bulk_actions": {
"bulk_actions": "批量操作",
@@ -1110,6 +1107,9 @@
"title": "内容宽度",
"default_description": "Trilium默认会限制内容的最大宽度以提高在宽屏中全屏时的可读性。",
"max_width_label": "内容最大宽度(像素)",
"apply_changes_description": "要应用内容宽度更改,请点击",
"reload_button": "重载前端",
"reload_description": "来自外观选项的更改",
"max_width_unit": "像素"
},
"native_title_bar": {
@@ -1917,7 +1917,7 @@
},
"custom_date_time_format": {
"title": "自定义日期/时间格式",
"description": "自定义通过 <shortcut /> 或工具栏插入的日期和时间格式有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
"description": "通过<shortcut />或工具栏的方式可自定义日期和时间格式有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
"format_string": "日期/时间格式字符串:",
"formatted_time": "格式化后日期/时间:"
},
@@ -2079,8 +2079,5 @@
"edit-slide": "编辑此幻灯片",
"start-presentation": "开始演示",
"slide-overview": "切换幻灯片概览"
},
"calendar_view": {
"delete_note": "删除笔记..."
}
}

View File

@@ -1104,6 +1104,9 @@
"title": "Inhaltsbreite",
"default_description": "Trilium begrenzt standardmäßig die maximale Inhaltsbreite, um die Lesbarkeit für maximierte Bildschirme auf Breitbildschirmen zu verbessern.",
"max_width_label": "Maximale Inhaltsbreite in Pixel",
"apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf",
"reload_button": "Frontend neu laden",
"reload_description": "Änderungen an den Darstellungsoptionen",
"max_width_unit": "Pixel"
},
"native_title_bar": {

View File

@@ -36,13 +36,10 @@
},
"branch_prefix": {
"edit_branch_prefix": "Edit branch prefix",
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
"help_on_tree_prefix": "Help on Tree prefix",
"prefix": "Prefix: ",
"save": "Save",
"branch_prefix_saved": "Branch prefix has been saved.",
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
"affected_branches": "Affected branches ({{count}}):"
"branch_prefix_saved": "Branch prefix has been saved."
},
"bulk_actions": {
"bulk_actions": "Bulk actions",
@@ -1111,7 +1108,9 @@
"default_description": "Trilium by default limits max content width to improve readability for maximized screens on wide screens.",
"max_width_label": "Max content width",
"max_width_unit": "pixels",
"centerContent": "Keep content centered"
"apply_changes_description": "To apply content width changes, click on",
"reload_button": "reload frontend",
"reload_description": "changes from appearance options"
},
"native_title_bar": {
"title": "Native Title Bar (requires app restart)",
@@ -1637,12 +1636,6 @@
"shared_locally": "This note is shared locally on {{- link}}.",
"help_link": "For help visit <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
},
"read-only-info": {
"read-only-note": "Currently viewing a read-only note.",
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
"auto-read-only-learn-more": "Learn more",
"edit-note": "Edit note"
},
"note_types": {
"text": "Text",
"code": "Code",
@@ -2041,9 +2034,6 @@
"start-presentation": "Start presentation",
"slide-overview": "Toggle an overview of the slides"
},
"calendar_view": {
"delete_note": "Delete note..."
},
"command_palette": {
"tree-action-name": "Tree: {{name}}",
"export_note_title": "Export Note",

View File

@@ -1107,7 +1107,10 @@
"title": "Ancho del contenido",
"default_description": "Trilium limita de forma predeterminada el ancho máximo del contenido para mejorar la legibilidad de ventanas maximizadas en pantallas anchas.",
"max_width_label": "Ancho máximo del contenido en píxeles",
"max_width_unit": "píxeles"
"max_width_unit": "píxeles",
"apply_changes_description": "Para aplicar cambios en el ancho del contenido, haga clic en",
"reload_button": "recargar la interfaz",
"reload_description": "cambios desde las opciones de apariencia"
},
"native_title_bar": {
"title": "Barra de título nativa (requiere reiniciar la aplicación)",
@@ -1589,7 +1592,7 @@
"tree-context-menu": {
"open-in-a-new-tab": "Abrir en nueva pestaña",
"open-in-a-new-split": "Abrir en nueva división",
"insert-note-after": "Insertar nota contigua",
"insert-note-after": "Insertar nota después de",
"insert-child-note": "Insertar subnota",
"delete": "Eliminar",
"search-in-subtree": "Buscar en subárbol",

View File

@@ -1106,6 +1106,9 @@
"title": "Largeur du contenu",
"default_description": "Trilium limite par défaut la largeur maximale du contenu pour améliorer la lisibilité sur des écrans larges.",
"max_width_label": "Largeur maximale du contenu en pixels",
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
"reload_button": "recharger l'interface",
"reload_description": "changements par rapport aux options d'apparence",
"max_width_unit": "Pixels"
},
"native_title_bar": {

View File

@@ -109,8 +109,7 @@
"export_type_single": "Solo questa nota, senza le sottostanti",
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
"opml_version_1": "OPML v.1.0 - solo testo semplice",
"opml_version_2": "OPML v2.0 - supporta anche HTML",
"share-format": "HTML per la pubblicazione sul web - utilizza lo stesso tema utilizzato per le note condivise, ma può essere pubblicato come sito web statico."
"opml_version_2": "OPML v2.0 - supporta anche HTML"
},
"password_not_set": {
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
@@ -1570,7 +1569,10 @@
"title": "Larghezza del contenuto",
"default_description": "Per impostazione predefinita, Trilium limita la larghezza massima del contenuto per migliorare la leggibilità sugli schermi più grandi.",
"max_width_label": "Larghezza massima del contenuto",
"max_width_unit": "pixel"
"max_width_unit": "pixel",
"apply_changes_description": "Per applicare le modifiche alla larghezza del contenuto, fare clic su",
"reload_button": "ricarica frontend",
"reload_description": "modifiche dalle opzioni di aspetto"
},
"native_title_bar": {
"title": "Barra del titolo nativa (richiede il riavvio dell'app)",

View File

@@ -39,10 +39,7 @@
"edit_branch_prefix": "ブランチ接頭辞の編集",
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
"prefix": "接頭辞: ",
"branch_prefix_saved": "ブランチの接頭辞が保存されました。",
"edit_branch_prefix_multiple": "{{count}} ブランチのブランチ接頭辞を編集",
"branch_prefix_saved_multiple": "{{count}} 個のブランチのブランチ接頭辞が保存されました。",
"affected_branches": "影響を受けるブランチ {{count}}:"
"branch_prefix_saved": "ブランチの接頭辞が保存されました。"
},
"global_menu": {
"menu": "メニュー",
@@ -833,10 +830,13 @@
"theme_defined": "テーマが定義されました"
},
"max_content_width": {
"reload_button": "フロントエンドをリロード",
"title": "コンテンツ幅",
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
"max_width_label": "最大コンテンツ幅",
"max_width_unit": "ピクセル"
"max_width_unit": "ピクセル",
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください",
"reload_description": "外観設定から変更"
},
"theme": {
"title": "アプリのテーマ",
@@ -2079,8 +2079,5 @@
"edit-slide": "このスライドを編集",
"start-presentation": "プレゼンテーションを開始",
"slide-overview": "スライドの概要を切り替え"
},
"calendar_view": {
"delete_note": "ノートを削除..."
}
}

View File

@@ -1464,7 +1464,10 @@
"title": "Szerokość zawartości",
"default_description": "Trilium domyślnie ogranicza maksymalną szerokość zawartości, aby poprawić czytelność na zmaksymalizowanych ekranach o dużej szerokości.",
"max_width_label": "Maksymalna szerokość zawartości",
"max_width_unit": "piksele"
"max_width_unit": "piksele",
"apply_changes_description": "Aby zastosować zmiany szerokości zawartości, kliknij na",
"reload_button": "przeładuj frontend",
"reload_description": "zmiany z opcji wyglądu"
},
"native_title_bar": {
"title": "Natywny pasek tytułu (wymaga ponownego uruchomienia aplikacji)",

View File

@@ -1082,7 +1082,10 @@
"title": "Largura do Conteúdo",
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em ecrãs largos.",
"max_width_label": "Largura máxima do conteúdo",
"max_width_unit": "pixels"
"max_width_unit": "pixels",
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
"reload_button": "recarregar frontend",
"reload_description": "alterações de opções de aparência"
},
"native_title_bar": {
"title": "Barra de Título Nativa (requer recarregar a app)",

View File

@@ -1304,6 +1304,9 @@
"title": "Largura do Conteúdo",
"max_width_label": "Largura máxima do conteúdo",
"max_width_unit": "pixels",
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
"reload_button": "recarregar frontend",
"reload_description": "alterações de opções de aparência",
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em telas wide."
},
"native_title_bar": {

View File

@@ -796,9 +796,12 @@
"modal_body_text": "Din cauza limitărilor la nivel de navigator, nu este posibilă citirea clipboard-ului din JavaScript. Inserați Markdown-ul pentru a-l importa în caseta de mai jos și dați clic pe butonul Import"
},
"max_content_width": {
"apply_changes_description": "Pentru a aplica schimbările de lățime a conținutului, dați click pe",
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
"max_width_label": "Lungimea maximă a conținutului",
"max_width_unit": "pixeli",
"reload_button": "reîncarcă interfața",
"reload_description": "schimbări din opțiunile de afișare",
"title": "Lățime conținut"
},
"mobile_detail_menu": {

View File

@@ -1203,8 +1203,11 @@
"max_content_width": {
"max_width_unit": "пикселей",
"title": "Ширина контентной области",
"reload_button": "перезагрузить интерфейс",
"default_description": "Trilium по умолчанию ограничивает максимальную ширину контента, чтобы улучшить читаемость на широких экранах.",
"max_width_label": "Максимальная ширина контентной области"
"max_width_label": "Максимальная ширина контентной области",
"apply_changes_description": "Чтобы применить изменения, нажмите на",
"reload_description": "изменения в параметрах внешнего вида"
},
"native_title_bar": {
"enabled": "включено",

View File

@@ -39,10 +39,7 @@
"help_on_tree_prefix": "有關樹前綴的說明",
"prefix": "前綴: ",
"save": "儲存",
"branch_prefix_saved": "已儲存分支前綴。",
"edit_branch_prefix_multiple": "編輯 {{count}} 個分支的前綴",
"branch_prefix_saved_multiple": "已為 {{count}} 個分支儲存分支前綴。",
"affected_branches": "受影響的分支 ({{count}}):"
"branch_prefix_saved": "已儲存分支前綴。"
},
"bulk_actions": {
"bulk_actions": "批次操作",
@@ -1107,6 +1104,9 @@
"title": "內容寬度",
"default_description": "Trilium 預設會限制內容的最大寬度以提高在寬螢幕中全螢幕時的可讀性。",
"max_width_label": "內容最大寬度(像素)",
"apply_changes_description": "要套用內容寬度更改,請點擊",
"reload_button": "重新載入前端",
"reload_description": "來自外觀選項的更改",
"max_width_unit": "像素"
},
"native_title_bar": {
@@ -2079,8 +2079,5 @@
"edit-slide": "編輯此投影片",
"start-presentation": "開始簡報",
"slide-overview": "切換投影片概覽"
},
"calendar_view": {
"delete_note": "刪除筆記…"
}
}

View File

@@ -1204,7 +1204,10 @@
"title": "Ширина вмісту",
"default_description": "Trilium за замовчуванням обмежує максимальну ширину вмісту, щоб поліпшити читабельність на широкоформатних екранах у режимі максимального розширення.",
"max_width_label": "Максимальна ширина вмісту",
"max_width_unit": "пікселів"
"max_width_unit": "пікселів",
"apply_changes_description": "Щоб застосувати зміни ширини вмісту, натисніть на",
"reload_button": "перезавантажити інтерфейс",
"reload_description": "зміни в параметрах зовнішнього вигляду"
},
"native_title_bar": {
"title": "Нативний рядок заголовка (потрібен перезапуск)",

View File

@@ -215,30 +215,6 @@ declare namespace Fancytree {
enableUpdate(enabled: boolean): void;
}
interface FancytreeNodeData {
noteId: string;
parentNoteId: string;
branchId: string;
isProtected: boolean;
noteType: NoteType;
}
interface FancytreeNewNode extends FancytreeNodeData {
title: string;
extraClasses: string;
icon: string;
refKey: string;
/** True if this node is loaded on demand, i.e. on first expansion. */
lazy: boolean;
/** Folder nodes have different default icons and click behavior. Note: Also non-folders may have children. */
folder: boolean;
/** Use isExpanded(), setExpanded() to access this property. */
expanded: boolean;
/** Node id (must be unique inside the tree) */
key: string;
children?: FancytreeNewNode[];
}
/** A FancytreeNode represents the hierarchical data model and operations. */
interface FancytreeNode {
// #region Properties
@@ -251,7 +227,7 @@ declare namespace Fancytree {
/** Display name (may contain HTML) */
title: string;
/** Contains all extra data that was passed on node creation */
data: FancytreeNodeData;
data: any;
/** Array of child nodes. For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array to define a node that has no children. */
children: FancytreeNode[];
/** Use isExpanded(), setExpanded() to access this property. */

View File

@@ -23,24 +23,6 @@ export class CssVarReader {
return (!isNaN(number.valueOf()) ? number.valueOf() : defaultValue)
}
asBoolean(defaultValue?: boolean) {
let value = this.value.toLocaleLowerCase().trim();
let result: boolean | undefined;
switch (value) {
case "true":
case "1":
result = true;
break;
case "false":
case "0":
result = false;
break;
}
return (result !== undefined) ? result : defaultValue;
}
asEnum<T>(enumType: T, defaultValue?: T[keyof T]): T[keyof T] | undefined {
let result: T[keyof T] | undefined;

View File

@@ -6,7 +6,7 @@
.floating-buttons-children,
.show-floating-buttons {
position: absolute;
top: var(--floating-buttons-vert-offset, 14px);
top: var(--floating-buttons-vert-offset, 10px);
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
display: flex;
flex-direction: row;

View File

@@ -1,6 +1,6 @@
import { t } from "i18next";
import "./FloatingButtons.css";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "./react/hooks";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean } from "./react/hooks";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { ParentComponent } from "./react/react_utils";
import { EventData, EventNames } from "../components/app_context";
@@ -20,7 +20,6 @@ interface FloatingButtonsProps {
* properly handle rounded corners, as defined by the --border-radius CSS variable.
*/
export default function FloatingButtons({ items }: FloatingButtonsProps) {
const [ top, setTop ] = useState(0);
const { note, noteContext } = useNoteContext();
const parentComponent = useContext(ParentComponent);
const [ viewType ] = useNoteLabel(note, "viewType");
@@ -48,14 +47,8 @@ export default function FloatingButtons({ items }: FloatingButtonsProps) {
const [ visible, setVisible ] = useState(true);
useEffect(() => setVisible(true), [ note ]);
useTriliumEvent("contentSafeMarginChanged", (e) => {
if (e.noteContext === noteContext) {
setTop(e.top);
}
});
return (
<div className="floating-buttons no-print" style={{top}}>
<div className="floating-buttons no-print">
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
{context && items.map((Component) => (
<Component {...context} />

View File

@@ -4,7 +4,7 @@ import Component from "../components/component";
import NoteContext from "../components/note_context";
import FNote from "../entities/fnote";
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
import server from "../services/server";
@@ -13,6 +13,8 @@ import toast from "../services/toast";
import { t } from "../services/i18n";
import { copyImageReferenceToClipboard } from "../services/image";
import tree from "../services/tree";
import protected_session_holder from "../services/protected_session_holder";
import options from "../services/options";
import { getHelpUrlForNote } from "../services/in_app_help";
import froca from "../services/froca";
import NoteLink from "./react/NoteLink";
@@ -99,26 +101,48 @@ function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingBut
/>
}
function EditButton({ note, noteContext }: FloatingButtonContext) {
const [animationClass, setAnimationClass] = useState("");
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
const isReadOnlyInfoBarDismissed = false; // TODO
function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
const [ animationClass, setAnimationClass ] = useState("");
const [ isEnabled, setIsEnabled ] = useState(false);
useEffect(() => {
if (isReadOnly) {
noteContext.isReadOnly().then(isReadOnly => {
setIsEnabled(
isDefaultViewMode
&& (!note.isProtected || protected_session_holder.isProtectedSessionAvailable())
&& !options.is("databaseReadonly")
&& isReadOnly
);
});
}, [ note ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) {
setIsEnabled(false);
}
});
// make the edit button stand out on the first display, otherwise
// it's difficult to notice that the note is readonly
useEffect(() => {
if (isEnabled) {
setAnimationClass("bx-tada bx-lg");
setTimeout(() => {
setAnimationClass("");
}, 1700);
}
}, [ isReadOnly ]);
}, [ isEnabled ]);
return !!isReadOnly && isReadOnlyInfoBarDismissed && <FloatingButton
return isEnabled && <FloatingButton
text={t("edit_button.edit_this_note")}
icon="bx bx-pencil"
className={animationClass}
onClick={() => enableEditing()}
onClick={() => {
if (noteContext.viewScope) {
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext });
}
}}
/>
}

View File

@@ -1,19 +0,0 @@
body.zen div.read-only-note-info-bar-widget {
width: fit-content;
max-width: var(--max-content-width);
border-radius: 8px;
border: unset;
margin: 0 auto 10px auto;
}
.read-only-note-info-bar-widget-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
:root div.read-only-note-info-bar-widget button {
white-space: nowrap;
padding: 2px 8px;
}

View File

@@ -1,36 +0,0 @@
import "./ReadOnlyNoteInfoBar.css";
import { t } from "../services/i18n";
import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks"
import Button from "./react/Button";
import InfoBar from "./react/InfoBar";
export default function ReadOnlyNoteInfoBar(props: {}) {
const {note, noteContext} = useNoteContext();
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
return <InfoBar className="read-only-note-info-bar-widget"
type={(isExplicitReadOnly ? "subtle" : "prominent")}
style={{display: (!isReadOnly) ? "none" : undefined}}>
<div class="read-only-note-info-bar-widget-content">
{(isExplicitReadOnly) ? (
<div>{t("read-only-info.read-only-note")}</div>
) : (
<div>
{t("read-only-info.auto-read-only-note")}
&nbsp;
<a class="tn-link"
href="https://docs.triliumnotes.org/user-guide/concepts/notes/read-only-notes#automatic-read-only-mode">
{t("read-only-info.auto-read-only-learn-more")}
</a>
</div>
)}
<Button text={t("read-only-info.edit-note")}
icon="bx-pencil" onClick={() => enableEditing()} />
</div>
</InfoBar>
}

View File

@@ -1,16 +1,9 @@
.note-list-widget {
min-height: 0;
max-width: var(--max-content-width); /* Inherited from .note-split */
overflow: auto;
contain: none !important;
}
body.prefers-centered-content .note-list-widget:not(.full-height) {
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
margin-inline: auto;
}
.note-list-widget .note-list {
padding: 10px;
}

View File

@@ -1,32 +0,0 @@
import { it, describe, expect } from "vitest";
import { buildNote } from "../../../test/easy-froca";
import { getBoardData } from "./data";
import FBranch from "../../../entities/fbranch";
import froca from "../../../services/froca";
describe("Board data", () => {
it("deduplicates cloned notes", async () => {
const parentNote = buildNote({
title: "Board",
"#collection": "",
"#viewType": "board",
children: [
{ id: "note1", title: "First note", "#status": "To Do" },
{ id: "note2", title: "Second note", "#status": "In progress" },
{ id: "note3", title: "Third note", "#status": "Done" }
]
});
const branch = new FBranch(froca, {
branchId: "note1_note2",
notePosition: 10,
fromSearchNote: false,
noteId: "note2",
parentNoteId: "note1"
});
froca.branches["note1_note2"] = branch;
froca.getNoteFromCache("note1").addChild("note2", "note1_note2", false);
const data = await getBoardData(parentNote, "status", {}, false);
const noteIds = Array.from(data.byColumn.values()).flat().map(item => item.note.noteId);
expect(noteIds.length).toBe(3);
});
});

View File

@@ -11,7 +11,7 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
const byColumn: ColumnMap = new Map();
// First, scan all notes to find what columns actually exist
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived, new Set<string>());
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived);
// Get all columns that exist in the notes
const columnsFromNotes = [...byColumn.keys()];
@@ -61,28 +61,26 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
};
}
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean, seenNoteIds: Set<string>) {
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean) {
for (const branch of branches) {
const note = await branch.getNote();
if (!note || (!includeArchived && note.isArchived)) continue;
if (note.type !== "search" && note.hasChildren()) {
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived, seenNoteIds);
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived);
}
const group = note.getLabelValue(groupByColumn);
if (!group || seenNoteIds.has(note.noteId)) {
if (!group) {
continue;
}
if (!byColumn.has(group)) {
byColumn.set(group, []);
}
byColumn.get(group)!.push({
branch,
note
});
seenNoteIds.add(note.noteId);
}
}

View File

@@ -58,6 +58,8 @@ export async function changeEvent(note: FNote, { startDate, endDate, startTime,
startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
if (startTime && endTime) {
setAttribute(note, "label", startAttribute, startTime);
setAttribute(note, "label", endAttribute, endTime);
}
}

View File

@@ -1,28 +0,0 @@
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),
})
}

View File

@@ -1,7 +1,6 @@
.calendar-view {
overflow: hidden;
position: relative;
outline: 0;
height: 100%;
user-select: none;
padding: 10px;
@@ -68,7 +67,6 @@
}
body.desktop:not(.zen) .calendar-view .calendar-header {
padding-block-start: 4px;
padding-inline-end: 5em;
}

View File

@@ -20,7 +20,6 @@ import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton";
import { RefObject } from "preact";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { openCalendarContextMenu } from "./context_menu";
interface CalendarViewData {
@@ -107,7 +106,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const plugins = usePlugins(isEditable, isCalendarRoot);
const locale = useLocale();
const { eventDidMount } = useEventDisplayCustomization(note);
const { eventDidMount } = useEventDisplayCustomization();
const editingProps = useEditing(note, isEditable, isCalendarRoot);
// React to changes.
@@ -197,11 +196,11 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
}
function useLocale() {
const [ formattingLocale ] = useTriliumOption("formattingLocale");
const [ locale ] = useTriliumOption("locale");
const [ calendarLocale, setCalendarLocale ] = useState<LocaleInput>();
useEffect(() => {
const correspondingLocale = LOCALE_MAPPINGS[formattingLocale];
const correspondingLocale = LOCALE_MAPPINGS[locale];
if (correspondingLocale) {
correspondingLocale().then((locale) => setCalendarLocale(locale.default));
} else {
@@ -254,7 +253,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
};
}
function useEventDisplayCustomization(parentNote: FNote) {
function useEventDisplayCustomization() {
const eventDidMount = useCallback((e: EventMountArg) => {
const { iconClass, promotedAttributes } = e.event.extendedProps;
@@ -303,11 +302,6 @@ function useEventDisplayCustomization(parentNote: FNote) {
}
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
}
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
const noteId = e.event.extendedProps.noteId;
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
});
}, []);
return { eventDidMount };
}

View File

@@ -1,62 +0,0 @@
import { EventData } from "../../components/app_context";
import BasicWidget from "../basic_widget";
import Container from "./container";
import NoteContext from "../../components/note_context";
export default class ContentHeader extends Container<BasicWidget> {
noteContext?: NoteContext;
thisElement?: HTMLElement;
parentElement?: HTMLElement;
resizeObserver: ResizeObserver;
currentHeight: number = 0;
currentSafeMargin: number = NaN;
constructor() {
super();
this.css("contain", "unset");
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
}
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
this.noteContext = noteContext;
this.init();
}
init() {
this.parentElement = this.parent?.$widget.get(0);
if (!this.parentElement) {
console.warn("No parent set for <ContentHeader>.");
return;
}
this.thisElement = this.$widget.get(0)!;
this.resizeObserver.observe(this.thisElement);
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
}
updateSafeMargin() {
const newSafeMargin = Math.max(this.currentHeight - this.parentElement!.scrollTop, 0);
if (newSafeMargin !== this.currentSafeMargin) {
this.currentSafeMargin = newSafeMargin;
this.triggerEvent("contentSafeMarginChanged", {
top: newSafeMargin,
noteContext: this.noteContext!
});
}
}
onResize(entries: ResizeObserverEntry[]) {
for (const entry of entries) {
if (entry.target === this.thisElement) {
this.currentHeight = entry.contentRect.height;
this.updateSafeMargin();
}
}
}
}

View File

@@ -1,10 +1,9 @@
import { EventData } from "../../components/app_context.js";
import { LOCALES } from "@triliumnext/commons";
import { readCssVar } from "../../utils/css-var.js";
import FlexContainer from "./flex_container.js";
import options from "../../services/options.js";
import type BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
import { LOCALES } from "@triliumnext/commons";
/**
* The root container is the top-most widget/container, from which the entire layout derives.
@@ -31,11 +30,9 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
}
this.#setMaxContentWidth();
this.#setMotion();
this.#setShadows();
this.#setBackdropEffects();
this.#setThemeCapabilities();
this.#setMotion(options.is("motionEnabled"));
this.#setShadows(options.is("shadowsEnabled"));
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
this.#setLocaleAndDirection(options.get("locale"));
return super.render();
@@ -43,21 +40,15 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("motionEnabled")) {
this.#setMotion();
this.#setMotion(options.is("motionEnabled"));
}
if (loadResults.isOptionReloaded("shadowsEnabled")) {
this.#setShadows();
this.#setShadows(options.is("shadowsEnabled"));
}
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
this.#setBackdropEffects();
}
if (loadResults.isOptionReloaded("maxContentWidth")
|| loadResults.isOptionReloaded("centerContent")) {
this.#setMaxContentWidth();
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
}
}
@@ -67,38 +58,19 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
}
#setMaxContentWidth() {
const width = Math.max(options.getInt("maxContentWidth") || 0, 640);
document.body.style.setProperty("--preferred-max-content-width", `${width}px`);
document.body.classList.toggle("prefers-centered-content", options.is("centerContent"));
}
#setMotion() {
const enabled = options.is("motionEnabled");
#setMotion(enabled: boolean) {
document.body.classList.toggle("motion-disabled", !enabled);
jQuery.fx.off = !enabled;
}
#setShadows() {
const enabled = options.is("shadowsEnabled");
#setShadows(enabled: boolean) {
document.body.classList.toggle("shadows-disabled", !enabled);
}
#setBackdropEffects() {
const enabled = options.is("backdropEffectsEnabled");
#setBackdropEffects(enabled: boolean) {
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
}
#setThemeCapabilities() {
// Supports background effects
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
.asBoolean(false);
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
}
#setLocaleAndDirection(locale: string) {
const correspondingLocale = LOCALES.find(l => l.id === locale);
document.body.lang = locale;

View File

@@ -1,13 +0,0 @@
.branch-prefix-dialog .branch-prefix-notes-list {
margin-top: 10px;
}
.branch-prefix-dialog .branch-prefix-notes-list ul {
max-height: 200px;
overflow: auto;
margin-top: 5px;
}
.branch-prefix-dialog .branch-prefix-current {
opacity: 0.6;
}

View File

@@ -10,26 +10,14 @@ import Button from "../react/Button.jsx";
import FormGroup from "../react/FormGroup.js";
import { useTriliumEvent } from "../react/hooks.jsx";
import FBranch from "../../entities/fbranch.js";
import type { ContextMenuCommandData } from "../../components/app_context.js";
import "./branch_prefix.css";
// Virtual branches (e.g., from search results) start with this prefix
const VIRTUAL_BRANCH_PREFIX = "virt-";
export default function BranchPrefixDialog() {
const [ shown, setShown ] = useState(false);
const [ branches, setBranches ] = useState<FBranch[]>([]);
const [ branch, setBranch ] = useState<FBranch>();
const [ prefix, setPrefix ] = useState("");
const branchInput = useRef<HTMLInputElement>(null);
useTriliumEvent("editBranchPrefix", async (data?: ContextMenuCommandData) => {
let branchIds: string[] = [];
if (data?.selectedOrActiveBranchIds && data.selectedOrActiveBranchIds.length > 0) {
// Multi-select mode from tree context menu
branchIds = data.selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith(VIRTUAL_BRANCH_PREFIX));
} else {
// Single branch mode from keyboard shortcut or when no selection
useTriliumEvent("editBranchPrefix", async () => {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
return;
@@ -41,8 +29,8 @@ export default function BranchPrefixDialog() {
return;
}
const branchId = await froca.getBranchId(parentNoteId, noteId);
if (!branchId) {
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
if (!newBranchId) {
return;
}
const parentNote = await froca.getNote(parentNoteId);
@@ -50,46 +38,25 @@ export default function BranchPrefixDialog() {
return;
}
branchIds = [branchId];
}
if (branchIds.length === 0) {
return;
}
const newBranches = branchIds
.map(id => froca.getBranch(id))
.filter((branch): branch is FBranch => branch !== null);
if (newBranches.length === 0) {
return;
}
setBranches(newBranches);
// Use the prefix of the first branch as the initial value
setPrefix(newBranches[0]?.prefix ?? "");
const newBranch = froca.getBranch(newBranchId);
setBranch(newBranch);
setPrefix(newBranch?.prefix ?? "");
setShown(true);
});
async function onSubmit() {
if (branches.length === 0) {
if (!branch) {
return;
}
if (branches.length === 1) {
await savePrefix(branches[0].branchId, prefix);
} else {
await savePrefixBatch(branches.map(b => b.branchId), prefix);
}
savePrefix(branch.branchId, prefix);
setShown(false);
}
const isSingleBranch = branches.length === 1;
return (
<Modal
className="branch-prefix-dialog"
title={isSingleBranch ? t("branch_prefix.edit_branch_prefix") : t("branch_prefix.edit_branch_prefix_multiple", { count: branches.length })}
title={t("branch_prefix.edit_branch_prefix")}
size="lg"
onShown={() => branchInput.current?.focus()}
onHidden={() => setShown(false)}
@@ -102,27 +69,9 @@ export default function BranchPrefixDialog() {
<div class="input-group">
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
{isSingleBranch && branches[0] && (
<div class="branch-prefix-note-title input-group-text"> - {branches[0].getNoteFromCache().title}</div>
)}
<div class="branch-prefix-note-title input-group-text"> - {branch && branch.getNoteFromCache().title}</div>
</div>
</FormGroup>
{!isSingleBranch && (
<div className="branch-prefix-notes-list">
<strong>{t("branch_prefix.affected_branches", { count: branches.length })}</strong>
<ul>
{branches.map((branch) => {
const note = branch.getNoteFromCache();
return (
<li key={branch.branchId}>
{branch.prefix && <span className="branch-prefix-current">{branch.prefix} - </span>}
{note.title}
</li>
);
})}
</ul>
</div>
)}
</Modal>
);
}
@@ -131,8 +80,3 @@ async function savePrefix(branchId: string, prefix: string) {
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
}
async function savePrefixBatch(branchIds: string[], prefix: string) {
await server.put("branches/set-prefix-batch", { branchIds, prefix });
toast.showMessage(t("branch_prefix.branch_prefix_saved_multiple", { count: branchIds.length }));
}

View File

@@ -8,11 +8,6 @@ import TypeWidget from "../type_widgets/type_widget.js";
const TPL = /*html*/`\
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<style>
/** Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. */
body.popup-editor-open > .modal-backdrop { z-index: 998; }
body.popup-editor-open .popup-editor-dialog { z-index: 999; }
body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
body.desktop .modal.popup-editor-dialog .modal-dialog {
max-width: 75vw;
}
@@ -62,7 +57,6 @@ const TPL = /*html*/`\
}
</style>
<div class="quick-edit-dialog-wrapper">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
@@ -77,7 +71,6 @@ const TPL = /*html*/`\
</div>
</div>
</div>
</div>
</div>
`;
@@ -86,7 +79,6 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
private noteContext: NoteContext;
private $modalHeader!: JQuery<HTMLElement>;
private $modalBody!: JQuery<HTMLElement>;
private $wrapper!: JQuery<HTMLDivElement>;
constructor() {
super();
@@ -101,7 +93,6 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
const $newWidget = $(TPL);
this.$modalHeader = $newWidget.find(".modal-title");
this.$modalBody = $newWidget.find(".modal-body");
this.$wrapper = $newWidget.find(".quick-edit-dialog-wrapper");
const children = this.$widget.children();
this.$modalHeader.append(children[0]);
@@ -121,27 +112,17 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
}
});
const colorClass = this.noteContext.note?.getColorClass();
const wrapperElement = this.$wrapper.get(0)!;
if (colorClass) {
wrapperElement.className = "quick-edit-dialog-wrapper " + colorClass;
} else {
wrapperElement.className = "quick-edit-dialog-wrapper";
}
const customHue = getComputedStyle(wrapperElement).getPropertyValue("--custom-color-hue");
if (customHue) {
/* Apply the tinted-dialog class only if the custom color CSS class specifies a hue */
wrapperElement.classList.add("tinted-quick-edit-dialog");
}
const activeEl = document.activeElement;
if (activeEl && "blur" in activeEl) {
(activeEl as HTMLElement).blur();
}
$dialog.on("shown.bs.modal", async () => {
// Reduce the z-index of modals so that ckeditor popups are properly shown on top of it.
// The backdrop instance is not shared so it's OK to make a one-off modification.
$("body > .modal-backdrop").css("z-index", "998");
$dialog.css("z-index", "999");
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
this.setVisibility(true);
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
@@ -162,12 +143,9 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
if (visible) {
$bodyItems.fadeIn();
this.$modalHeader.children().show();
document.body.classList.add("popup-editor-open");
} else {
$bodyItems.hide();
this.$modalHeader.children().hide();
document.body.classList.remove("popup-editor-open");
}
}

View File

@@ -39,14 +39,12 @@ const TPL = /*html*/`
<div class="note-detail">
<style>
.note-detail {
max-width: var(--max-content-width); /* Inherited from .note-split */
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
}
body.prefers-centered-content .note-detail {
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
margin-inline: auto;
.note-detail.full-height {
height: 100%;
}
</style>
</div>

View File

@@ -173,6 +173,14 @@ interface ExpandedSubtreeResponse {
branchIds: string[];
}
interface Node extends Fancytree.NodeData {
noteId: string;
parentNoteId: string;
branchId: string;
isProtected: boolean;
noteType: NoteType;
}
interface RefreshContext {
noteIdsToUpdate: Set<string>;
noteIdsToReload: Set<string>;
@@ -761,7 +769,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
prepareChildren(parentNote: FNote) {
utils.assertArguments(parentNote);
const noteList: Fancytree.FancytreeNewNode[] = [];
const noteList: Node[] = [];
const hideArchivedNotes = this.hideArchivedNotes;
@@ -829,7 +837,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const isFolder = note.isFolder();
const node: Fancytree.FancytreeNewNode = {
const node: Node = {
noteId: note.noteId,
parentNoteId: branch.parentNoteId,
branchId: branch.branchId,
@@ -841,7 +849,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
refKey: note.noteId,
lazy: true,
folder: isFolder,
expanded: !!branch.isExpanded && note.type !== "search",
expanded: branch.isExpanded && note.type !== "search",
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
@@ -903,6 +911,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return extraClasses.join(" ");
}
/** @returns {FancytreeNode[]} */
getSelectedNodes(stopOnParents = false) {
return this.tree.getSelectedNodes(stopOnParents);
}
@@ -1523,7 +1532,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// Automatically expand the hoisted note by default
const node = this.getActiveNode();
if (node && node.data.noteId === this.noteContext.hoistedNoteId){
if (node?.data.noteId === this.noteContext.hoistedNoteId){
this.setExpanded(node.data.branchId, true);
}
}
@@ -1582,20 +1591,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.clearSelectedNodes();
}
async editBranchPrefixCommand({ node }: CommandListenerData<"editBranchPrefix">) {
const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-"));
if (!branchIds.length) {
return;
}
// Trigger the event with the selected branch IDs
appContext.triggerEvent("editBranchPrefix", {
selectedOrActiveBranchIds: branchIds,
node: node
});
}
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
if (node.data.noteId === "root") {
return false;

View File

@@ -52,7 +52,6 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
const note = this.noteContext?.note;
if (!note) {
this.$widget.addClass("bgfx empty-note");
return;
}
@@ -62,7 +61,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass(utils.getNoteTypeClass(note.type));
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
this.$widget.toggleClass("protected", note.isProtected);
const noteLanguage = note?.getLabelValue("language");
@@ -71,7 +70,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
}
#isFullWidthNote(note: FNote) {
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
return true;
}

View File

@@ -27,16 +27,15 @@ interface CKEditorOpts {
onClick?: (e: MouseEvent, pos?: ModelPosition | null) => void;
onKeyDown?: (e: KeyboardEvent) => void;
onBlur?: () => void;
onInitialized?: (editorInstance: CKTextEditor) => void;
}
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, onInitialized, ...restProps }: CKEditorOpts) {
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, ...restProps }: CKEditorOpts) {
const editorContainerRef = useRef<HTMLDivElement>(null);
const textEditorRef = useRef<CKTextEditor>(null);
useImperativeHandle(apiRef, () => {
return {
focus() {
textEditorRef.current?.editing.view.focus();
editorContainerRef.current?.focus();
textEditorRef.current?.model.change((writer) => {
const documentRoot = textEditorRef.current?.editing.model.document.getRoot();
if (documentRoot) {
@@ -84,8 +83,6 @@ export default function CKEditor({ apiRef, currentValue, editor, config, disable
if (currentValue) {
textEditor.setData(currentValue);
}
onInitialized?.(textEditor);
});
}, []);

View File

@@ -1,23 +0,0 @@
.info-bar {
--link-color: currentColor;
margin-top: 4px;
contain: unset !important;
padding: 8px 20px;
color: var(--read-only-note-info-bar-color);
font-size: .9em;
cursor: default;
user-select: none;
}
.info-bar-prominent {
background: var(--alert-bar-background, var(--accented-background-color));
}
.info-bar-subtle {
color: var(--muted-text-color);
border-bottom: 1px solid var(--main-border-color);
margin-block: 0;
margin-inline: 10px;
padding-inline: 12px;
}

View File

@@ -1,19 +0,0 @@
import "./InfoBar.css";
import { ComponentChildren, CSSProperties } from "preact";
export type InfoBarParams = {
type: "prominent" | "subtle",
className: string;
style: CSSProperties
children: ComponentChildren;
};
export default function InfoBar(props: InfoBarParams) {
return <div className={`info-bar ${props.className} info-bar-${props.type}`} style={props.style}>
{props?.children}
</div>
}
InfoBar.defaultProps = {
type: "prominent"
} as InfoBarParams

View File

@@ -1,26 +1,25 @@
import { CSSProperties } from "preact/compat";
import { DragData } from "../note_tree";
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
import { Inputs, MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandListenerData, EventData, EventNames } from "../../components/app_context";
import { ParentComponent } from "./react_utils";
import SpacedUpdate from "../../services/spaced_update";
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
import options, { type OptionValue } from "../../services/options";
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
import NoteContext from "../../components/note_context";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import FBlob from "../../entities/fblob";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { RefObject, VNode } from "preact";
import { Tooltip } from "bootstrap";
import { ViewMode } from "../../services/link";
import appContext, { CommandListenerData, EventData, EventNames } from "../../components/app_context";
import attributes from "../../services/attributes";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
import Component from "../../components/component";
import FBlob from "../../entities/fblob";
import FNote from "../../entities/fnote";
import { CSSProperties } from "preact/compat";
import keyboard_actions from "../../services/keyboard_actions";
import Mark from "mark.js";
import NoteContext from "../../components/note_context";
import NoteContextAwareWidget from "../note_context_aware_widget";
import options, { type OptionValue } from "../../services/options";
import protected_session_holder from "../../services/protected_session_holder";
import SpacedUpdate from "../../services/spaced_update";
import { DragData } from "../note_tree";
import Component from "../../components/component";
import toast, { ToastOptions } from "../../services/toast";
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
import { ViewMode } from "../../services/link";
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentComponent = useContext(ParentComponent);
@@ -199,7 +198,6 @@ export function useNoteContext() {
const [ notePath, setNotePath ] = useState<string | null | undefined>();
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ , setViewMode ] = useState<ViewMode>();
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
const [ refreshCounter, setRefreshCounter ] = useState(0);
useEffect(() => {
@@ -219,11 +217,6 @@ export function useNoteContext() {
setRefreshCounter(refreshCounter + 1);
}
});
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
if (eventNoteContext.ntxId === noteContext?.ntxId) {
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
}
});
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
@@ -237,8 +230,7 @@ export function useNoteContext() {
viewScope: noteContext?.viewScope,
componentId: parentComponent.componentId,
noteContext,
parentComponent,
isReadOnlyTemporarilyDisabled
parentComponent
};
}
@@ -709,51 +701,3 @@ export function useResizeObserver(ref: RefObject<HTMLElement>, callback: () => v
return () => observer.disconnect();
}, [ callback, ref ]);
}
/**
* Indicates that the current note is in read-only mode, while an editing mode is available,
* and provides a way to switch to editing mode.
*/
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
const [isReadOnly, setIsReadOnly] = useState<boolean | undefined>(undefined);
const enableEditing = useCallback(() => {
if (noteContext?.viewScope) {
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
}
}, [noteContext]);
useEffect(() => {
if (note && noteContext) {
isNoteReadOnly(note, noteContext).then((readOnly) => {
setIsReadOnly(readOnly);
});
}
}, [note, noteContext]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) {
setIsReadOnly(false);
}
});
return {isReadOnly, enableEditing};
}
async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
if (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) {
return false;
}
if (options.is("databaseReadonly")) {
return false;
}
if (noteContext.viewScope?.viewMode !== "default" || !await noteContext.isReadOnly()) {
return false;
}
return true;
}

View File

@@ -41,7 +41,7 @@ export default function EditedNotesTab({ note }: TabContext) {
)}
</span>
)
}), " ")}
}))}
</div>
) : (
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>

View File

@@ -1,20 +1,19 @@
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import { ParentComponent } from "../react/react_utils";
import { t } from "../../services/i18n"
import { useContext } from "preact/hooks";
import { useIsNoteReadOnly } from "../react/hooks";
import ActionButton from "../react/ActionButton"
import appContext, { CommandNames } from "../../components/app_context";
import branches from "../../services/branches";
import dialog from "../../services/dialog";
import Dropdown from "../react/Dropdown";
import FNote from "../../entities/fnote"
import NoteContext from "../../components/note_context";
import dialog from "../../services/dialog";
import { t } from "../../services/i18n"
import server from "../../services/server";
import toast from "../../services/toast";
import ws from "../../services/ws";
import ActionButton from "../react/ActionButton"
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import { ParentComponent } from "../react/react_utils";
import { useContext } from "preact/hooks";
import NoteContext from "../../components/note_context";
import branches from "../../services/branches";
interface NoteActionsProps {
note?: FNote;
@@ -53,7 +52,6 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type);
const isSearchOrBook = ["search", "book"].includes(note.type);
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
return (
<Dropdown
@@ -61,14 +59,8 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
className="note-actions"
hideToggleArrow
noSelectButtonStyle
iconAction>
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
command={() => enableEditing()} />
<FormDropdownDivider />
</>}
iconAction
>
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
import "./style.css";
import { Indexed, numberObjectsInPlace } from "../../services/utils";
import { numberObjectsInPlace } from "../../services/utils";
import { EventNames } from "../../components/app_context";
import NoteActions from "./NoteActions";
import { KeyboardActionNames } from "@triliumnext/commons";
@@ -11,47 +11,30 @@ import { TabConfiguration, TitleContext } from "./ribbon-interface";
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
interface ComputedTab extends Indexed<TabConfiguration> {
shouldShow: boolean;
}
export default function Ribbon() {
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
const noteType = useNoteProperty(note, "type");
const titleContext: TitleContext = { note };
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
const [ computedTabs, setComputedTabs ] = useState<ComputedTab[]>();
const titleContext: TitleContext = useMemo(() => ({
note,
noteContext
}), [ note, noteContext ]);
async function refresh() {
const computedTabs: ComputedTab[] = [];
for (const tab of TAB_CONFIGURATION) {
const shouldShow = await shouldShowTab(tab.show, titleContext);
computedTabs.push({
const computedTabs = useMemo(
() => TAB_CONFIGURATION.map(tab => {
const shouldShow = typeof tab.show === "boolean" ? tab.show : tab.show?.(titleContext);
return {
...tab,
shouldShow: !!shouldShow
});
shouldShow
}
setComputedTabs(computedTabs);
}
useEffect(() => {
refresh();
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
}),
[ titleContext, note, noteType ]);
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
useEffect(() => {
if (!computedTabs) return;
const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)));
setActiveTabIndex(tabToActivate?.index);
}, [ computedTabs, note?.noteId ]);
}, [ note?.noteId ]);
// Register keyboard shortcuts.
const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []);
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
if (!computedTabs) return;
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
if (correspondingTab) {
if (activeTabIndex !== correspondingTab.index) {
@@ -68,7 +51,7 @@ export default function Ribbon() {
<>
<div className="ribbon-top-row">
<div className="ribbon-tab-container">
{computedTabs && computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
{computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
shouldShow && <RibbonTab
icon={icon}
title={typeof title === "string" ? title : title(titleContext)}
@@ -91,7 +74,7 @@ export default function Ribbon() {
</div>
<div className="ribbon-body-container">
{computedTabs && computedTabs.map(tab => {
{computedTabs.map(tab => {
const isActive = tab.index === activeTabIndex;
if (!isActive && !tab.stayInDom) {
return;
@@ -146,9 +129,3 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
)
}
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {
if (showConfig === null || showConfig === undefined) return true;
if (typeof showConfig === "boolean") return showConfig;
if ("then" in showConfig) return await showConfig(context);
return showConfig(context);
}

View File

@@ -21,9 +21,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
{
title: t("classic_editor_toolbar.title"),
icon: "bx bx-text",
show: async ({ note, noteContext }) => note?.type === "text"
&& options.get("textNoteEditorType") === "ckeditor-classic"
&& !(await noteContext?.isReadOnly()),
show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic",
toggleCommand: "toggleRibbonTabClassicEditor",
content: FormattingToolbar,
activate: true,

View File

@@ -238,6 +238,11 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
}
});
// Focus on show.
useEffect(() => {
setTimeout(() => editorRef.current?.focus(), 0);
}, []);
// Interaction with CKEditor.
useLegacyImperativeHandlers(useMemo(() => ({
loadReferenceLinkTitle: async ($el: JQuery<HTMLElement>, href: string) => {
@@ -358,7 +363,6 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
}}
onKeyDown={() => attributeDetailWidget.hide()}
onBlur={() => save()}
onInitialized={() => editorRef.current?.focus()}
disableNewlines disableSpellcheck
/>

View File

@@ -1,9 +1,8 @@
import { ComponentChildren } from "preact";
import { useNoteContext } from "../../react/hooks";
import { TabContext } from "../ribbon-interface";
import { TabContext, TitleContext } from "../ribbon-interface";
import { useEffect, useMemo, useState } from "preact/hooks";
import { RIBBON_TAB_DEFINITIONS } from "../RibbonDefinition";
import { shouldShowTab } from "../Ribbon";
interface StandaloneRibbonAdapterProps {
component: (props: TabContext) => ComponentChildren;
@@ -17,11 +16,10 @@ export default function StandaloneRibbonAdapter({ component }: StandaloneRibbonA
const Component = component;
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
const definition = useMemo(() => RIBBON_TAB_DEFINITIONS.find(def => def.content === component), [ component ]);
const [ shown, setShown ] = useState<boolean | null | undefined>(false);
const [ shown, setShown ] = useState(unwrapShown(definition?.show, { note }));
useEffect(() => {
if (!definition) return;
shouldShowTab(definition.show, { note, noteContext }).then(setShown);
setShown(unwrapShown(definition?.show, { note }));
}, [ note ]);
return (
@@ -37,3 +35,9 @@ export default function StandaloneRibbonAdapter({ component }: StandaloneRibbonA
/>
);
}
function unwrapShown(value: boolean | ((context: TitleContext) => boolean | null | undefined) | undefined, context: TitleContext) {
if (!value) return true;
if (typeof value === "boolean") return value;
return !!value(context);
}

View File

@@ -16,14 +16,13 @@ export interface TabContext {
export interface TitleContext {
note: FNote | null | undefined;
noteContext: NoteContext | undefined;
}
export interface TabConfiguration {
title: string | ((context: TitleContext) => string);
icon: string;
content: (context: TabContext) => VNode | false;
show: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined);
show: boolean | ((context: TitleContext) => boolean | null | undefined);
toggleCommand?: KeyboardActionNames;
activate?: boolean | ((context: TitleContext) => boolean);
/**

View File

@@ -1,7 +0,0 @@
.shared-info-widget {
display: flex;
}
.shared-info-widget button {
margin-inline-start: 8px;
}

View File

@@ -1,12 +1,11 @@
import "./shared_info.css";
import { t } from "../services/i18n";
import { useEffect, useState } from "preact/hooks";
import { t } from "../services/i18n";
import Alert from "./react/Alert";
import { useNoteContext, useTriliumEvent, useTriliumOption } from "./react/hooks";
import attributes from "../services/attributes";
import FNote from "../entities/fnote";
import HelpButton from "./react/HelpButton";
import InfoBar from "./react/InfoBar";
import attributes from "../services/attributes";
import RawHtml from "./react/RawHtml";
import HelpButton from "./react/HelpButton";
export default function SharedInfo() {
const { note } = useNoteContext();
@@ -36,7 +35,7 @@ export default function SharedInfo() {
link = `${location.protocol}//${host}${location.pathname}share/${shareId}`;
}
setLink(`<a href="${link}" class="external tn-link">${link}</a>`);
setLink(`<a href="${link}" class="external">${link}</a>`);
}
useEffect(refresh, [ note ]);
@@ -49,14 +48,20 @@ export default function SharedInfo() {
});
return (
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
<Alert className="shared-info-widget" type="warning" style={{
contain: "none",
margin: "10px",
padding: "10px",
fontWeight: "bold",
display: !link ? "none" : undefined
}}>
{link && (
<RawHtml html={syncServerHost
? t("shared_info.shared_publicly", { link })
: t("shared_info.shared_locally", { link })} />
)}
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
</InfoBar>
</Alert>
)
}

View File

@@ -13,10 +13,6 @@ import protected_session_holder from "../../services/protected_session_holder.js
const TPL = /*html*/`
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
<style>
.canvas-widget {
height: 100%;
}
.excalidraw .App-menu_top .buttonList {
display: flex;
}

View File

@@ -284,8 +284,7 @@ function SmoothScrollEnabledOption() {
}
function MaxContentWidth() {
const [maxContentWidth, setMaxContentWidth] = useTriliumOption("maxContentWidth");
const [centerContent, setCenterContent] = useTriliumOptionBool("centerContent");
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
return (
<OptionsSection title={t("max_content_width.title")}>
@@ -301,9 +300,9 @@ function MaxContentWidth() {
</FormGroup>
</Column>
<FormCheckbox label={t("max_content_width.centerContent")}
currentValue={centerContent}
onChange={setCenterContent} />
<p>
{t("max_content_width.apply_changes_description")} <Button text={t("max_content_width.reload_button")} size="micro" onClick={reloadFrontendApp} />
</p>
</OptionsSection>
)
}

View File

@@ -97,7 +97,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
return (
tokens.length ? (
<div style={{ overflow: "auto"}}>
<div style={{ overflow: "auto", height: "500px"}}>
<table className="table table-stripped">
<thead>
<tr>

View File

@@ -72,8 +72,8 @@ function EditorFeatures() {
return (
<OptionsSection title={t("editorfeatures.title")}>
<EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.note_completion_description")} />
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.slash_commands_description")} />
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.emoji_completion_description")} />
</OptionsSection>
);
}

View File

@@ -33,7 +33,6 @@ const TPL = /*html*/`
body.heading-style-underline .note-detail-readonly-text h6 { border-bottom: 1px solid var(--main-border-color); }
.note-detail-readonly-text {
padding-inline-start: 24px;
padding-top: 10px;
font-family: var(--detail-font-family);

View File

@@ -103,7 +103,7 @@ const linkOverlays = [
];
const TPL = /*html*/`
<div class="note-detail-relation-map full-height note-detail-printable">
<div class="note-detail-relation-map note-detail-printable">
<div class="relation-map-wrapper">
<div class="relation-map-container"></div>
</div>

View File

@@ -108,8 +108,6 @@ const config: ForgeConfig = {
"--share=network",
// System notifications with libnotify
"--talk-name=org.freedesktop.Notifications",
// System tray
"--talk-name=org.kde.StatusNotifierWatcher"
],
modules: [
{

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/desktop",
"version": "0.99.4",
"version": "0.99.3",
"description": "Build your personal knowledge base with Trilium Notes",
"private": true,
"main": "src/main.ts",
@@ -16,7 +16,7 @@
"build": "tsx scripts/build.ts",
"start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
"electron-forge:make": "pnpm build && electron-forge make dist",
"electron-forge:make-flatpak": "pnpm build && DEBUG=* electron-forge make dist --targets=@electron-forge/maker-flatpak",
"electron-forge:make-flatpak": "pnpm build && electron-forge make dist --targets=@electron-forge/maker-flatpak",
"electron-forge:package": "pnpm build && electron-forge package dist",
"electron-forge:start": "pnpm build && electron-forge start dist",
"e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test"
@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "38.6.0",
"electron": "38.5.0",
"@electron-forge/cli": "7.10.2",
"@electron-forge/maker-deb": "7.10.2",
"@electron-forge/maker-dmg": "7.10.2",

View File

@@ -39,16 +39,11 @@ async function main() {
app.commandLine.appendSwitch("disable-smooth-scrolling");
}
if (process.platform === "linux") {
app.setName(PRODUCT_NAME);
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
// See https://github.com/electron/electron/issues/46538 for more info.
if (process.platform === "linux") {
app.setName(PRODUCT_NAME);
app.commandLine.appendSwitch("gtk-version", "3");
// Enable global shortcuts in Flatpak
// the app runs in a Wayland session.
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
}
// Quit when all windows are closed, except on macOS. There, it's common

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.99.3",
"appVersion": "0.99.2",
"files": [
{
"isClone": false,
@@ -2700,7 +2700,6 @@
}
],
"format": "html",
"dataFileName": "Note Types.html",
"attachments": [],
"dirFileName": "Note Types",
"children": [

View File

@@ -270,7 +270,7 @@
</li>
</ul>
</li>
<li><a href="root/Trilium%20Demo/Note%20Types.html" target="detail">Note Types</a>
<li>Note Types
<ul>
<li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a>
</li>

View File

@@ -14,7 +14,6 @@
<div class="ck-content">
<h2>☑️ Tasks</h2>
<ul>
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
</ul>

View File

@@ -14,10 +14,11 @@
<div class="ck-content">
<figure class="image image-style-align-right image_resized" style="width:29.84%;">
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg"
width="150" height="150">
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg" width="150"
height="150">
</figure>
<p><strong>Welcome to Trilium Notes!</strong>
</p>
<p>This is a "demo" document packaged with Trilium to showcase some of its
features and also give you some ideas on how you might structure your notes.
@@ -25,17 +26,22 @@
you wish.</p>
<p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or
our <a href="https://github.com/TriliumNext">GitHub repository</a>
</p>
<h2>Cleanup</h2>
<p>Once you're finished with experimenting and want to cleanup these pages,
you can simply delete them all.</p>
<h2>Formatting</h2>
<p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or&nbsp;
<a
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
<h3>Lists</h3>
<p><strong>Ordered:</strong>
</p>
<ol>
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
@@ -50,6 +56,7 @@
</li>
</ol>
<p><strong>Unordered:</strong>
</p>
<ul>
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
@@ -60,6 +67,7 @@
</li>
</ul>
<h3>Block quotes</h3>
<blockquote>
<p>Whereof one cannot speak, thereof one must be silent”</p>
<p> Ludwig Wittgenstein</p>
@@ -67,9 +75,9 @@
<hr>
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>,
<a
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>, <a href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>,
<a
href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and <a href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
</div>
</div>
</body>

View File

@@ -21,12 +21,8 @@
language, should that fail it is possible to manually adjust it. The color
scheme for the syntax highlighting is adjustable in settings.&nbsp;</p><pre><code class="language-application-javascript-env-frontend">function helloWorld() {
alert("Hello world");
}</code></pre>
<p>For larger pieces of code it is better to use a code note, which uses
a fully-fledged code editor (CodeMirror). For an example of a code note,

View File

@@ -1,21 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../style.css">
<base target="_parent">
<title data-trilium-title>Note Types</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Note Types</h1>
<div class="ck-content">
<p>T</p>
</div>
</div>
</body>
</html>

View File

@@ -13,8 +13,9 @@
<h1 data-trilium-h1>Task manager</h1>
<div class="ck-content">
<p>This is a simple TODO/Task manager. See the <a href="https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases/task-manager">Trilium documentation</a> for
information on how it works.</p>
<p>This is a simple TODO/Task manager. You can see some description and explanation
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a>
</p>
<p>Please note that this is meant as scripting example only and feature/bug
support is very limited.</p>
</div>

View File

@@ -16,32 +16,18 @@
<p>Documentation: <a href="http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html">http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html</a>
</p><pre><code class="language-text-x-sh">#!/bin/bash
# This script opens 4 terminal windows.
i="0"
while [ $i -lt 4 ]
do
xterm &amp;
i=$[$i+1]
done</code></pre>
</div>
</div>

View File

@@ -12,11 +12,11 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "38.6.0",
"electron": "38.5.0",
"fs-extra": "11.3.2"
},
"scripts": {
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts",
"edit-demo": "cross-env TRILIUM_PORT=37744 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
"edit-demo": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
}
}

View File

@@ -1,502 +0,0 @@
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");
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/server",
"version": "0.99.4",
"version": "0.99.3",
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
"private": true,
"main": "./src/main.ts",
@@ -67,7 +67,7 @@
"@types/xml2js": "0.4.14",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"axios": "1.13.2",
"axios": "1.13.1",
"bindings": "1.5.0",
"bootstrap": "5.3.8",
"chardet": "2.1.1",
@@ -81,7 +81,7 @@
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "3.1.10",
"electron": "38.6.0",
"electron": "38.5.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
@@ -97,7 +97,7 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.6.1",
"i18next": "25.6.0",
"i18next-fs-backend": "2.6.0",
"image-type": "6.0.0",
"ini": "6.0.0",
@@ -105,17 +105,17 @@
"is-svg": "6.1.0",
"jimp": "1.6.0",
"js-yaml": "4.1.0",
"marked": "16.4.2",
"marked": "16.4.1",
"mime-types": "3.0.1",
"multer": "2.0.2",
"normalize-strings": "1.1.1",
"ollama": "0.6.2",
"openai": "6.8.1",
"openai": "6.7.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.17.0",
"sax": "1.4.3",
"sax": "1.4.1",
"serve-favicon": "2.5.1",
"stream-throttle": "0.1.3",
"strip-bom": "5.0.0",
@@ -126,7 +126,7 @@
"tmp": "0.2.5",
"turndown": "7.2.2",
"unescape": "1.0.1",
"vite": "7.2.2",
"vite": "7.1.12",
"ws": "8.18.3",
"xml2js": "0.6.2",
"yauzl": "3.2.0"

View File

@@ -20,10 +20,10 @@ fi
# Debug output
echo "Selected Arch: $ARCH"
script_dir=$(realpath $(dirname $0))
# Set Node.js version and architecture-specific filename
NODE_VERSION=$(cat "../../.nvmrc")
NODE_VERSION=22.16.0
script_dir=$(realpath $(dirname $0))
BUILD_DIR="$script_dir/../dist"
DIST_DIR="$script_dir/../out"

View File

@@ -20,8 +20,9 @@ describe("etapi/search", () => {
content = randomUUID();
await createNote(app, token, content);
});
}, 30000); // Increase timeout to 30 seconds for app initialization
describe("Basic Search", () => {
it("finds by content", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}&debug=true`)
@@ -37,4 +38,335 @@ describe("etapi/search", () => {
.expect(200);
expect(response.body.results).toHaveLength(0);
});
it("returns proper response structure", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body).toHaveProperty("results");
expect(Array.isArray(response.body.results)).toBe(true);
if (response.body.results.length > 0) {
const note = response.body.results[0];
expect(note).toHaveProperty("noteId");
expect(note).toHaveProperty("title");
expect(note).toHaveProperty("type");
}
});
it("returns debug info when requested", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${content}&debug=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body).toHaveProperty("debugInfo");
expect(response.body.debugInfo).toBeTruthy();
});
it("returns 400 for missing search parameter", async () => {
await supertest(app)
.get("/etapi/notes")
.auth(USER, token, { "type": "basic"})
.expect(400);
});
it("returns 400 for empty search parameter", async () => {
await supertest(app)
.get("/etapi/notes?search=")
.auth(USER, token, { "type": "basic"})
.expect(400);
});
});
describe("Search Parameters", () => {
let testNoteId: string;
beforeAll(async () => {
// Create a test note with unique content
const uniqueContent = `test-${randomUUID()}`;
testNoteId = await createNote(app, token, uniqueContent);
}, 10000);
it("respects fastSearch parameter", async () => {
// Fast search should not find by content
const fastResponse = await supertest(app)
.get(`/etapi/notes?search=${content}&fastSearch=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(fastResponse.body.results).toHaveLength(0);
// Regular search should find by content
const regularResponse = await supertest(app)
.get(`/etapi/notes?search=${content}&fastSearch=false`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(regularResponse.body.results.length).toBeGreaterThan(0);
});
it("respects includeArchivedNotes parameter", async () => {
// Default should include archived notes
const withArchivedResponse = await supertest(app)
.get(`/etapi/notes?search=*&includeArchivedNotes=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const withoutArchivedResponse = await supertest(app)
.get(`/etapi/notes?search=*&includeArchivedNotes=false`)
.auth(USER, token, { "type": "basic"})
.expect(200);
// Note: Actual behavior depends on whether there are archived notes
expect(withArchivedResponse.body.results).toBeDefined();
expect(withoutArchivedResponse.body.results).toBeDefined();
});
it("respects limit parameter", async () => {
const limit = 5;
const response = await supertest(app)
.get(`/etapi/notes?search=*&limit=${limit}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results.length).toBeLessThanOrEqual(limit);
});
it("handles fuzzyAttributeSearch parameter", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=*&fuzzyAttributeSearch=true`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
});
describe("Search Queries", () => {
let titleNoteId: string;
let labelNoteId: string;
beforeAll(async () => {
// Create test notes with specific attributes
const uniqueTitle = `SearchTest-${randomUUID()}`;
// Create note with specific title
const titleResponse = await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic"})
.send({
"parentNoteId": "root",
"title": uniqueTitle,
"type": "text",
"content": "Title test content"
})
.expect(201);
titleNoteId = titleResponse.body.note.noteId;
// Create note with label
const labelResponse = await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic"})
.send({
"parentNoteId": "root",
"title": "Label Test",
"type": "text",
"content": "Label test content"
})
.expect(201);
labelNoteId = labelResponse.body.note.noteId;
// Add label to note
await supertest(app)
.post("/etapi/attributes")
.auth(USER, token, { "type": "basic"})
.send({
"noteId": labelNoteId,
"type": "label",
"name": "testlabel",
"value": "testvalue"
})
.expect(201);
}, 15000); // 15 second timeout for setup
it("searches by title", async () => {
// Get the title we created
const noteResponse = await supertest(app)
.get(`/etapi/notes/${titleNoteId}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const title = noteResponse.body.title;
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(title)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === titleNoteId);
expect(foundNote).toBeTruthy();
});
it("searches by label", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === labelNoteId);
expect(foundNote).toBeTruthy();
});
it("searches by label with value", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel=testvalue")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
const foundNote = searchResponse.body.results.find((n: any) => n.noteId === labelNoteId);
expect(foundNote).toBeTruthy();
});
it("handles complex queries with AND operator", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel AND note.type=text")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results).toBeDefined();
});
it("handles queries with OR operator", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel OR #nonexistent")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
});
it("handles queries with NOT operator", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#testlabel NOT #nonexistent")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results.length).toBeGreaterThan(0);
});
it("handles wildcard searches", async () => {
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=note.type%3Dtext&limit=10`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results).toBeDefined();
// Should return results if any text notes exist
expect(Array.isArray(searchResponse.body.results)).toBe(true);
});
it("handles empty results gracefully", async () => {
const nonexistentQuery = `nonexistent-${randomUUID()}`;
const searchResponse = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(nonexistentQuery)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(searchResponse.body.results).toHaveLength(0);
});
});
describe("Error Handling", () => {
it("handles invalid query syntax gracefully", async () => {
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("(((")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
// Should return empty results or handle error gracefully
expect(response.body.results).toBeDefined();
});
it("requires authentication", async () => {
await supertest(app)
.get(`/etapi/notes?search=test`)
.expect(401);
});
it("rejects invalid authentication", async () => {
await supertest(app)
.get(`/etapi/notes?search=test`)
.auth(USER, "invalid-token", { "type": "basic"})
.expect(401);
});
});
describe("Performance", () => {
it("handles large result sets", async () => {
const startTime = Date.now();
const response = await supertest(app)
.get(`/etapi/notes?search=*&limit=100`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const endTime = Date.now();
const duration = endTime - startTime;
expect(response.body.results).toBeDefined();
// Search should complete in reasonable time (5 seconds)
expect(duration).toBeLessThan(5000);
});
it("handles queries efficiently", async () => {
const startTime = Date.now();
await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent("#*")}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
const endTime = Date.now();
const duration = endTime - startTime;
// Attribute search should be fast
expect(duration).toBeLessThan(3000);
});
});
describe("Special Characters", () => {
it("handles special characters in search", async () => {
const specialChars = "test@#$%";
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(specialChars)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
it("handles unicode characters", async () => {
const unicode = "测试";
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(unicode)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
it("handles quotes in search", async () => {
const quoted = '"test phrase"';
const response = await supertest(app)
.get(`/etapi/notes?search=${encodeURIComponent(quoted)}`)
.auth(USER, token, { "type": "basic"})
.expect(200);
expect(response.body.results).toBeDefined();
});
});
});

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More