mirror of
https://github.com/zadam/trilium.git
synced 2025-11-10 07:15:51 +01:00
Compare commits
23 Commits
v0.99.4
...
feat/rice-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f1773609f | ||
|
|
da0302066d | ||
|
|
942647ab9c | ||
|
|
b8aa7402d8 | ||
|
|
052e28ab1b | ||
|
|
16912e606e | ||
|
|
321752ac18 | ||
|
|
10988095c2 | ||
|
|
253da139de | ||
|
|
d992a5e4a2 | ||
|
|
58c225237c | ||
|
|
d074841885 | ||
|
|
06b2d71b27 | ||
|
|
0afb8a11c8 | ||
|
|
f529ddc601 | ||
|
|
8572f82e0a | ||
|
|
b09a2c386d | ||
|
|
7c5553bd4b | ||
|
|
37d0136c50 | ||
|
|
5b79e0d71e | ||
|
|
053f722cb8 | ||
|
|
21aaec2c38 | ||
|
|
1db4971da6 |
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -499,10 +499,6 @@ type EventMappings = {
|
||||
noteIds: string[];
|
||||
};
|
||||
refreshData: { ntxId: string | null | undefined };
|
||||
contentSafeMarginChanged: {
|
||||
top: number;
|
||||
noteContext: NoteContext;
|
||||
}
|
||||
};
|
||||
|
||||
export type EventListener<T extends EventNames> = {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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} />)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "boxicons/css/boxicons.min.css";
|
||||
|
||||
:root {
|
||||
--print-font-size: 11pt;
|
||||
--ck-content-color-image-caption-background: transparent !important;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ for (let i = 1; i <= 19; i++) {
|
||||
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
||||
"Delete",
|
||||
"Enter",
|
||||
"NumpadEnter",
|
||||
...functionKeyCodes
|
||||
]);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
.note-detail-relation-map {
|
||||
height: 100%;
|
||||
overflow: hidden !important;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -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-list-widget:not(.full-height) {
|
||||
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 {
|
||||
@@ -2550,18 +2436,4 @@ 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%;
|
||||
}
|
||||
@@ -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,9 +192,9 @@
|
||||
--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;
|
||||
--floating-button-color: var(--button-text-color);
|
||||
@@ -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);
|
||||
@@ -298,10 +294,4 @@ 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%);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -274,10 +270,4 @@
|
||||
* The --custom-color-hue variable contains the hue of the user-selected note color.
|
||||
* 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%);
|
||||
}
|
||||
@@ -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 .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%);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -674,17 +679,4 @@ 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ div.note-detail-empty {
|
||||
--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 */
|
||||
@@ -173,19 +173,16 @@ div.note-detail-empty {
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -196,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;
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
div.promoted-attributes-container {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-inline-start: 12px;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": "مفعل",
|
||||
|
||||
@@ -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": "删除笔记..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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": "ノートを削除..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": "включено",
|
||||
|
||||
@@ -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": "刪除筆記…"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Нативний рядок заголовка (потрібен перезапуск)",
|
||||
|
||||
26
apps/client/src/types-fancytree.d.ts
vendored
26
apps/client/src/types-fancytree.d.ts
vendored
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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")}
|
||||
|
||||
<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>
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -10,86 +10,53 @@ 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
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
||||
|
||||
if (!noteId || !parentNoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
if (!branchId) {
|
||||
return;
|
||||
}
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
if (!parentNote || parentNote.type === "search") {
|
||||
return;
|
||||
}
|
||||
|
||||
branchIds = [branchId];
|
||||
}
|
||||
|
||||
if (branchIds.length === 0) {
|
||||
useTriliumEvent("editBranchPrefix", async () => {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newBranches = branchIds
|
||||
.map(id => froca.getBranch(id))
|
||||
.filter((branch): branch is FBranch => branch !== null);
|
||||
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
||||
|
||||
if (newBranches.length === 0) {
|
||||
if (!noteId || !parentNoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBranches(newBranches);
|
||||
// Use the prefix of the first branch as the initial value
|
||||
setPrefix(newBranches[0]?.prefix ?? "");
|
||||
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
|
||||
if (!newBranchId) {
|
||||
return;
|
||||
}
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
if (!parentNote || parentNote.type === "search") {
|
||||
return;
|
||||
}
|
||||
|
||||
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 }));
|
||||
}
|
||||
|
||||
@@ -57,19 +57,17 @@ 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">
|
||||
<div class="modal-title">
|
||||
<!-- This is where the first child will be injected -->
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<!-- This is where the first child will be injected -->
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- This is where all but the first child will be injected. -->
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- This is where all but the first child will be injected. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,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();
|
||||
@@ -96,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]);
|
||||
@@ -116,21 +112,6 @@ 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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -52,7 +52,6 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
|
||||
const note = this.noteContext?.note;
|
||||
if (!note) {
|
||||
this.$widget.addClass("bgfx");
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ export default function EditedNotesTab({ note }: TabContext) {
|
||||
useEffect(() => {
|
||||
if (!note) return;
|
||||
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
|
||||
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
|
||||
const noteIds = editedNotes.flatMap((n) => n.noteId);
|
||||
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
|
||||
const noteIds = editedNotes.flatMap((n) => n.noteId);
|
||||
await froca.getNotes(noteIds, true); // preload all at once
|
||||
setEditedNotes(editedNotes);
|
||||
});
|
||||
@@ -41,11 +41,11 @@ export default function EditedNotesTab({ note }: TabContext) {
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}), " ")}
|
||||
}))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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")} />
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
setComputedTabs(computedTabs);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
||||
shouldShow
|
||||
}
|
||||
}),
|
||||
[ 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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
/**
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.shared-info-widget {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.shared-info-widget button {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
3
apps/edit-docs/demo/!!!meta.json
vendored
3
apps/edit-docs/demo/!!!meta.json
vendored
@@ -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": [
|
||||
|
||||
2
apps/edit-docs/demo/navigation.html
vendored
2
apps/edit-docs/demo/navigation.html
vendored
@@ -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>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<h2>☑️ Tasks</h2>
|
||||
|
||||
<ul>
|
||||
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
|
||||
</ul>
|
||||
|
||||
16
apps/edit-docs/demo/root/Trilium Demo.html
vendored
16
apps/edit-docs/demo/root/Trilium Demo.html
vendored
@@ -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
|
||||
<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>
|
||||
|
||||
@@ -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. </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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 &
|
||||
|
||||
|
||||
|
||||
i=$[$i+1]
|
||||
|
||||
|
||||
|
||||
done</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -20,21 +20,353 @@ 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`)
|
||||
.auth(USER, token, { "type": "basic"})
|
||||
.expect(200);
|
||||
expect(response.body.results).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not find by content when fast search is on", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes?search=${content}&debug=true&fastSearch=true`)
|
||||
.auth(USER, token, { "type": "basic"})
|
||||
.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);
|
||||
});
|
||||
});
|
||||
|
||||
it("finds by content", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes?search=${content}&debug=true`)
|
||||
.auth(USER, token, { "type": "basic"})
|
||||
.expect(200);
|
||||
expect(response.body.results).toHaveLength(1);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not find by content when fast search is on", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes?search=${content}&debug=true&fastSearch=true`)
|
||||
.auth(USER, token, { "type": "basic"})
|
||||
.expect(200);
|
||||
expect(response.body.results).toHaveLength(0);
|
||||
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.
@@ -146,9 +146,228 @@ CREATE INDEX IDX_notes_blobId on notes (blobId);
|
||||
CREATE INDEX IDX_revisions_blobId on revisions (blobId);
|
||||
CREATE INDEX IDX_attachments_blobId on attachments (blobId);
|
||||
|
||||
-- Strategic Performance Indexes from migration 234
|
||||
-- NOTES TABLE INDEXES
|
||||
CREATE INDEX IDX_notes_search_composite
|
||||
ON notes (isDeleted, type, mime, dateModified DESC);
|
||||
|
||||
CREATE INDEX IDX_notes_metadata_covering
|
||||
ON notes (noteId, isDeleted, type, mime, title, dateModified, isProtected);
|
||||
|
||||
CREATE INDEX IDX_notes_protected_deleted
|
||||
ON notes (isProtected, isDeleted)
|
||||
WHERE isProtected = 1;
|
||||
|
||||
-- BRANCHES TABLE INDEXES
|
||||
CREATE INDEX IDX_branches_tree_traversal
|
||||
ON branches (parentNoteId, isDeleted, notePosition);
|
||||
|
||||
CREATE INDEX IDX_branches_covering
|
||||
ON branches (noteId, parentNoteId, isDeleted, notePosition, prefix);
|
||||
|
||||
CREATE INDEX IDX_branches_note_parents
|
||||
ON branches (noteId, isDeleted)
|
||||
WHERE isDeleted = 0;
|
||||
|
||||
-- ATTRIBUTES TABLE INDEXES
|
||||
CREATE INDEX IDX_attributes_search_composite
|
||||
ON attributes (name, value, isDeleted);
|
||||
|
||||
CREATE INDEX IDX_attributes_covering
|
||||
ON attributes (noteId, name, value, type, isDeleted, position);
|
||||
|
||||
CREATE INDEX IDX_attributes_inheritable
|
||||
ON attributes (isInheritable, isDeleted)
|
||||
WHERE isInheritable = 1 AND isDeleted = 0;
|
||||
|
||||
CREATE INDEX IDX_attributes_labels
|
||||
ON attributes (type, name, value)
|
||||
WHERE type = 'label' AND isDeleted = 0;
|
||||
|
||||
CREATE INDEX IDX_attributes_relations
|
||||
ON attributes (type, name, value)
|
||||
WHERE type = 'relation' AND isDeleted = 0;
|
||||
|
||||
-- BLOBS TABLE INDEXES
|
||||
CREATE INDEX IDX_blobs_content_size
|
||||
ON blobs (blobId, LENGTH(content));
|
||||
|
||||
-- ATTACHMENTS TABLE INDEXES
|
||||
CREATE INDEX IDX_attachments_composite
|
||||
ON attachments (ownerId, role, isDeleted, position);
|
||||
|
||||
-- REVISIONS TABLE INDEXES
|
||||
CREATE INDEX IDX_revisions_note_date
|
||||
ON revisions (noteId, utcDateCreated DESC);
|
||||
|
||||
-- ENTITY_CHANGES TABLE INDEXES
|
||||
CREATE INDEX IDX_entity_changes_sync
|
||||
ON entity_changes (isSynced, utcDateChanged);
|
||||
|
||||
CREATE INDEX IDX_entity_changes_component
|
||||
ON entity_changes (componentId, utcDateChanged DESC);
|
||||
|
||||
-- RECENT_NOTES TABLE INDEXES
|
||||
CREATE INDEX IDX_recent_notes_date
|
||||
ON recent_notes (utcDateCreated DESC);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
expires INTEGER
|
||||
);
|
||||
|
||||
-- FTS5 Full-Text Search Support
|
||||
-- Create FTS5 virtual table with trigram tokenizer
|
||||
-- Trigram tokenizer provides language-agnostic substring matching:
|
||||
-- 1. Fast substring matching (50-100x speedup for LIKE queries without wildcards)
|
||||
-- 2. Case-insensitive search without custom collation
|
||||
-- 3. No language-specific stemming assumptions (works for all languages)
|
||||
-- 4. Boolean operators (AND, OR, NOT) and phrase matching with quotes
|
||||
--
|
||||
-- IMPORTANT: Trigram requires minimum 3-character tokens for matching
|
||||
-- detail='none' reduces index size by ~50% while maintaining MATCH/rank performance
|
||||
-- (loses position info for highlight() function, but snippet() still works)
|
||||
CREATE VIRTUAL TABLE notes_fts USING fts5(
|
||||
noteId UNINDEXED,
|
||||
title,
|
||||
content,
|
||||
tokenize = 'trigram',
|
||||
detail = 'none'
|
||||
);
|
||||
|
||||
-- Triggers to keep FTS table synchronized with notes
|
||||
-- IMPORTANT: These triggers must handle all SQL operations including:
|
||||
-- - Regular INSERT/UPDATE/DELETE
|
||||
-- - INSERT OR REPLACE
|
||||
-- - INSERT ... ON CONFLICT ... DO UPDATE (upsert)
|
||||
-- - Cases where notes are created before blobs (import scenarios)
|
||||
|
||||
-- Trigger for INSERT operations on notes
|
||||
-- Handles: INSERT, INSERT OR REPLACE, INSERT OR IGNORE, and the INSERT part of upsert
|
||||
CREATE TRIGGER notes_fts_insert
|
||||
AFTER INSERT ON notes
|
||||
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
AND NEW.isDeleted = 0
|
||||
AND NEW.isProtected = 0
|
||||
BEGIN
|
||||
-- First delete any existing FTS entry (in case of INSERT OR REPLACE)
|
||||
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
|
||||
|
||||
-- Then insert the new entry, using LEFT JOIN to handle missing blobs
|
||||
INSERT INTO notes_fts (noteId, title, content)
|
||||
SELECT
|
||||
NEW.noteId,
|
||||
NEW.title,
|
||||
COALESCE(b.content, '') -- Use empty string if blob doesn't exist yet
|
||||
FROM (SELECT NEW.noteId) AS note_select
|
||||
LEFT JOIN blobs b ON b.blobId = NEW.blobId;
|
||||
END;
|
||||
|
||||
-- Trigger for UPDATE operations on notes table
|
||||
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
|
||||
-- Fires for ANY update to searchable notes to ensure FTS stays in sync
|
||||
CREATE TRIGGER notes_fts_update
|
||||
AFTER UPDATE ON notes
|
||||
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
-- Fire on any change, not just specific columns, to handle all upsert scenarios
|
||||
BEGIN
|
||||
-- Always delete the old entry
|
||||
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
|
||||
|
||||
-- Insert new entry if note is not deleted and not protected
|
||||
INSERT INTO notes_fts (noteId, title, content)
|
||||
SELECT
|
||||
NEW.noteId,
|
||||
NEW.title,
|
||||
COALESCE(b.content, '') -- Use empty string if blob doesn't exist yet
|
||||
FROM (SELECT NEW.noteId) AS note_select
|
||||
LEFT JOIN blobs b ON b.blobId = NEW.blobId
|
||||
WHERE NEW.isDeleted = 0
|
||||
AND NEW.isProtected = 0;
|
||||
END;
|
||||
|
||||
-- Trigger for UPDATE operations on blobs
|
||||
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
|
||||
-- IMPORTANT: Uses INSERT OR REPLACE for efficiency with deduplicated blobs
|
||||
CREATE TRIGGER notes_fts_blob_update
|
||||
AFTER UPDATE ON blobs
|
||||
BEGIN
|
||||
-- Use INSERT OR REPLACE for atomic update of all notes sharing this blob
|
||||
-- This is more efficient than DELETE + INSERT when many notes share the same blob
|
||||
INSERT OR REPLACE INTO notes_fts (noteId, title, content)
|
||||
SELECT
|
||||
n.noteId,
|
||||
n.title,
|
||||
NEW.content
|
||||
FROM notes n
|
||||
WHERE n.blobId = NEW.blobId
|
||||
AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
AND n.isDeleted = 0
|
||||
AND n.isProtected = 0;
|
||||
END;
|
||||
|
||||
-- Trigger for DELETE operations
|
||||
CREATE TRIGGER notes_fts_delete
|
||||
AFTER DELETE ON notes
|
||||
BEGIN
|
||||
DELETE FROM notes_fts WHERE noteId = OLD.noteId;
|
||||
END;
|
||||
|
||||
-- Trigger for soft delete (isDeleted = 1)
|
||||
CREATE TRIGGER notes_fts_soft_delete
|
||||
AFTER UPDATE ON notes
|
||||
WHEN OLD.isDeleted = 0 AND NEW.isDeleted = 1
|
||||
BEGIN
|
||||
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
|
||||
END;
|
||||
|
||||
-- Trigger for notes becoming protected
|
||||
-- Remove from FTS when a note becomes protected
|
||||
CREATE TRIGGER notes_fts_protect
|
||||
AFTER UPDATE ON notes
|
||||
WHEN OLD.isProtected = 0 AND NEW.isProtected = 1
|
||||
BEGIN
|
||||
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
|
||||
END;
|
||||
|
||||
-- Trigger for notes becoming unprotected
|
||||
-- Add to FTS when a note becomes unprotected (if eligible)
|
||||
CREATE TRIGGER notes_fts_unprotect
|
||||
AFTER UPDATE ON notes
|
||||
WHEN OLD.isProtected = 1 AND NEW.isProtected = 0
|
||||
AND NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
AND NEW.isDeleted = 0
|
||||
BEGIN
|
||||
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
|
||||
|
||||
INSERT INTO notes_fts (noteId, title, content)
|
||||
SELECT
|
||||
NEW.noteId,
|
||||
NEW.title,
|
||||
COALESCE(b.content, '')
|
||||
FROM (SELECT NEW.noteId) AS note_select
|
||||
LEFT JOIN blobs b ON b.blobId = NEW.blobId;
|
||||
END;
|
||||
|
||||
-- Trigger for INSERT operations on blobs
|
||||
-- Handles: INSERT, INSERT OR REPLACE, and the INSERT part of upsert
|
||||
-- Updates all notes that reference this blob (common during import and deduplication)
|
||||
CREATE TRIGGER notes_fts_blob_insert
|
||||
AFTER INSERT ON blobs
|
||||
BEGIN
|
||||
-- Use INSERT OR REPLACE to handle both new and existing FTS entries
|
||||
-- This is crucial for blob deduplication where multiple notes may already
|
||||
-- exist that reference this blob before the blob itself is created
|
||||
INSERT OR REPLACE INTO notes_fts (noteId, title, content)
|
||||
SELECT
|
||||
n.noteId,
|
||||
n.title,
|
||||
NEW.content
|
||||
FROM notes n
|
||||
WHERE n.blobId = NEW.blobId
|
||||
AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
AND n.isDeleted = 0
|
||||
AND n.isProtected = 0;
|
||||
END;
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
27
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
27
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI.html
generated
vendored
@@ -36,7 +36,7 @@ class="image image_resized" style="width:74.04%;">
|
||||
<p>To see what embedding models Ollama has available, you can check out
|
||||
<a
|
||||
href="https://ollama.com/search?c=embedding">this search</a>on their website, and then <code>pull</code> whichever one
|
||||
you want to try out. A popular choice is <code>mxbai-embed-large</code>.</p>
|
||||
you want to try out. As of 4/15/25, my personal favorite is <code>mxbai-embed-large</code>.</p>
|
||||
<p>First, we'll need to select the Ollama provider from the tabs of providers,
|
||||
then we will enter in the Base URL for our Ollama. Since our Ollama is
|
||||
running on our local machine, our Base URL is <code>http://localhost:11434</code>.
|
||||
@@ -145,18 +145,17 @@ class="image image_resized" style="width:74.04%;">
|
||||
<p>You don't need to tell the LLM to execute a certain tool, it should “smartly”
|
||||
call tools and automatically execute them as needed.</p>
|
||||
<h2>Overview</h2>
|
||||
<p>To start, simply press the <em>Chat with Notes</em> button in the
|
||||
<a
|
||||
class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>.</p>
|
||||
<figure class="image image_resized" style="width:60.77%;">
|
||||
<p>Now that you know about embeddings and tools, you can just go ahead and
|
||||
use the “Chat with Notes” button, where you can go ahead and start chatting!:</p>
|
||||
<figure
|
||||
class="image image_resized" style="width:60.77%;">
|
||||
<img style="aspect-ratio:1378/539;" src="2_AI_image.png"
|
||||
width="1378" height="539">
|
||||
</figure>
|
||||
<p>If you don't see the button in the <a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>,
|
||||
you might need to move it from the <em>Available Launchers</em> section to
|
||||
the <em>Visible Launchers</em> section:</p>
|
||||
<figure class="image image_resized"
|
||||
style="width:69.81%;">
|
||||
<img style="aspect-ratio:1765/1287;" src="9_AI_image.png"
|
||||
width="1765" height="1287">
|
||||
</figure>
|
||||
</figure>
|
||||
<p>If you don't see the “Chat with Notes” button on your side launchbar,
|
||||
you might need to move it from the “Available Launchers” section to the
|
||||
“Visible Launchers” section:</p>
|
||||
<figure class="image image_resized" style="width:69.81%;">
|
||||
<img style="aspect-ratio:1765/1287;" src="9_AI_image.png"
|
||||
width="1765" height="1287">
|
||||
</figure>
|
||||
@@ -8,7 +8,7 @@
|
||||
<li>
|
||||
<p><a class="reference-link" href="#root/_help_HI6GBBIduIgv">Labels</a> can
|
||||
be used for a variety of purposes, such as storing metadata or configuring
|
||||
the behavior of notes. Labels are also searchable, enhancing note retrieval.</p>
|
||||
the behaviour of notes. Labels are also searchable, enhancing note retrieval.</p>
|
||||
<p>For more information, including predefined labels, see <a class="reference-link"
|
||||
href="#root/_help_HI6GBBIduIgv">Labels</a>.</p>
|
||||
</li>
|
||||
@@ -21,7 +21,7 @@
|
||||
class="reference-link" href="#root/_help_Cq5X6iKQop6R">Relations</a>.</p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>These attributes play a crucial role in organizing, categorizing, and
|
||||
<p>These attributes play a crucial role in organizing, categorising, and
|
||||
enhancing the functionality of notes.</p>
|
||||
<h2>Viewing the list of attributes</h2>
|
||||
<p>Both the labels and relations for the current note are displayed in the <em>Owned Attributes</em> section
|
||||
|
||||
@@ -11,7 +11,7 @@ const {secret, title, content} = req.body;
|
||||
if (req.method == 'POST' && secret === 'secret-password') {
|
||||
// notes must be saved somewhere in the tree hierarchy specified by a parent note.
|
||||
// This is defined by a relation from this code note to the "target" parent note
|
||||
// alternatively you can just use constant noteId for simplicity (get that from "Note Info" dialog of the desired parent note)
|
||||
// alternetively you can just use constant noteId for simplicity (get that from "Note Info" dialog of the desired parent note)
|
||||
const targetParentNoteId = api.currentNote.getRelationValue('targetNote');
|
||||
|
||||
const {note} = api.createTextNote(targetParentNoteId, title, content);
|
||||
@@ -30,7 +30,7 @@ else {
|
||||
be saved</li>
|
||||
</ul>
|
||||
<h3>Explanation</h3>
|
||||
<p>Let's test this by using an HTTP client to send a request:</p><pre><code class="language-text-x-trilium-auto">POST http://your-trilium-server/custom/create-note
|
||||
<p>Let's test this by using an HTTP client to send a request:</p><pre><code class="language-text-x-trilium-auto">POST http://my.trilium.org/custom/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -64,12 +64,12 @@ Content-Type: application/json
|
||||
can always look into its <a href="https://expressjs.com/en/api.html">documentation</a> for
|
||||
details.</p>
|
||||
<h3>Parameters</h3>
|
||||
<p>REST request paths often contain parameters in the URL, e.g.:</p><pre><code class="language-text-x-trilium-auto">http://your-trilium-server/custom/notes/123</code></pre>
|
||||
<p>REST request paths often contain parameters in the URL, e.g.:</p><pre><code class="language-text-x-trilium-auto">http://my.trilium.org/custom/notes/123</code></pre>
|
||||
<p>The last part is dynamic so the matching of the URL must also be dynamic
|
||||
- for this reason the matching is done with regular expressions. Following <code>customRequestHandler</code> value
|
||||
would match it:</p><pre><code class="language-text-x-trilium-auto">notes/([0-9]+)</code></pre>
|
||||
<p>Additionally, this also defines a matching group with the use of parenthesis
|
||||
which then makes it easier to extract the value. The matched groups are
|
||||
available in <code>api.pathParams</code>:</p><pre><code class="language-text-x-trilium-auto">const noteId = api.pathParams[0];</code></pre>
|
||||
<p>Often you also need query params (as in e.g. <code>http://your-trilium-server/custom/notes?noteId=123</code>),
|
||||
<p>Often you also need query params (as in e.g. <code>http://my.trilium.org/custom/notes?noteId=123</code>),
|
||||
you can get those with standard express <code>req.query.noteId</code>.</p>
|
||||
@@ -1,35 +0,0 @@
|
||||
<p>Nightly releases are versions built every day, containing the latest improvements
|
||||
and bugfixes, directly from the main development branch. These versions
|
||||
are generally useful in preparation for a release, to ensure that there
|
||||
are no significant bugs that need to be addressed first, or they can be
|
||||
used to confirm whether a particular bug is fixed or feature is well implemented.</p>
|
||||
<h2>Regarding the stability</h2>
|
||||
<p>Despite being on a development branch, generally the main branch is pretty
|
||||
stable since PRs are tested before they are merged. If you notice any issues,
|
||||
feel free to report them either via a ticket or via the Matrix.</p>
|
||||
<h2>Downloading the nightly release manually</h2>
|
||||
<p>Go to <a href="https://github.com/TriliumNext/Trilium/releases/tag/nightly">github.com/TriliumNext/Trilium/releases/tag/nightly</a> and
|
||||
look for the artifacts starting with <code>TriliumNotes-main</code>. Choose
|
||||
the appropriate one for your platform (e.g. <code>windows-x64.zip</code>).</p>
|
||||
<p>Depending on your use case, you can either test the portable version or
|
||||
even use the installer.</p>
|
||||
<aside class="admonition note">
|
||||
<p>If you choose the installable version (e.g. the .exe on Windows), it will
|
||||
replace your stable installation.</p>
|
||||
</aside>
|
||||
<aside class="admonition important">
|
||||
<p>By default, the nightly uses the same database as the production version.
|
||||
Generally you could easily downgrade if needed. However, if there are changes
|
||||
to the database or sync version, it will not be possible to downgrade without
|
||||
having to restore from a backup.</p>
|
||||
</aside>
|
||||
<h2>Automatically download and install the latest nightly</h2>
|
||||
<p>This is pretty useful if you are a beta tester that wants to periodically
|
||||
update their version:</p>
|
||||
<p>On Ubuntu:</p><pre><code class="language-text-x-trilium-auto">#!/usr/bin/env bash
|
||||
|
||||
name=TriliumNotes-linux-x64-nightly.deb
|
||||
rm -f $name*
|
||||
wget https://github.com/TriliumNext/Trilium/releases/download/nightly/$name
|
||||
sudo apt-get install ./$name
|
||||
rm $name</code></pre>
|
||||
@@ -1,41 +0,0 @@
|
||||
<aside class="admonition warning">
|
||||
<p>This functionality is still in preview, expect possible issues or even
|
||||
the feature disappearing completely.
|
||||
<br>Feel free to <a href="#root/_help_wy8So3yZZlH9">report</a> any issues you might
|
||||
have.</p>
|
||||
</aside>
|
||||
<p>The read-only database is an alternative to <a class="reference-link"
|
||||
href="#root/_help_R9pX4DGra2Vt">Sharing</a> notes. Although the share functionality
|
||||
works pretty well to publish pages to the Internet in a wiki, blog-like
|
||||
format it does not offer the full functionality behind Trilium (such as
|
||||
the advanced <a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a> or
|
||||
the interactivity behind <a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a> or
|
||||
the various <a class="reference-link" href="#root/_help_KSZ04uQ2D1St">Note Types</a>).</p>
|
||||
<p>When the database is in read-only mode, the Trilium application can be
|
||||
used as normal, but editing is disabled and changes are made in-memory
|
||||
only.</p>
|
||||
<h2>What it does</h2>
|
||||
<ul>
|
||||
<li>All notes are read-only, without the possibility of editing them.</li>
|
||||
<li>Features that would normally alter the database such as the list of recent
|
||||
notes are disabled.</li>
|
||||
</ul>
|
||||
<h2>Limitations</h2>
|
||||
<ul>
|
||||
<li>Some features might “slip through” and still end up creating a note, for
|
||||
example.
|
||||
<ul>
|
||||
<li>However, the database is still read-only, so all modifications will be
|
||||
reset if the server is restarted.</li>
|
||||
<li>Whenever this occurs, <code>ERROR: read-only DB ignored</code> will be shown
|
||||
in the logs.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Setting a database as read-only</h2>
|
||||
<p>First, make sure the database is initialized (e.g. the first set up is
|
||||
complete). Then modify the <a href="#root/_help_Gzjqa934BdH4">config.ini</a> by
|
||||
looking for the <code>[General]</code> section and adding a new <code>readOnly</code> field:</p><pre><code class="language-text-x-trilium-auto">[General]
|
||||
readOnly=true</code></pre>
|
||||
<p>If your server is already running, restart it to apply the changes.</p>
|
||||
<p>Similarly, to disable read-only remove the line or set it to <code>false</code>.</p>
|
||||
12
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Safe mode.html
generated
vendored
12
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Safe mode.html
generated
vendored
@@ -1,12 +0,0 @@
|
||||
<p>Safe mode is triggered by setting the <code>TRILIUM_SAFE_MODE</code> environment
|
||||
variable to a truthy value, usually <code>1</code>.</p>
|
||||
<p>In each artifact there is a <code>trilium-safe-mode.sh</code> (or <code>.bat</code>)
|
||||
script to enable it.</p>
|
||||
<p>What it does:</p>
|
||||
<ul>
|
||||
<li>Disables <code>customWidget</code> launcher types in <code>app/widgets/containers/launcher.js</code>.</li>
|
||||
<li>Disables the running of <code>mobileStartup</code> or <code>frontendStartup</code> scripts.</li>
|
||||
<li>Displays the root note instead of the previously saved session.</li>
|
||||
<li>Disables the running of <code>backendStartup</code>, <code>hourly</code>, <code>daily</code> scripts
|
||||
and checks for the hidden subtree.</li>
|
||||
</ul>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user