From 61df0f3d3140174a444e571446679b9ee369eda9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 15 Dec 2025 13:16:14 +0200 Subject: [PATCH 01/17] feat(breadcrumb): trim path when hoisted --- apps/client/src/widgets/layout/Breadcrumb.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index 5f0397c7e..085b92ef7 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -20,7 +20,7 @@ const INITIAL_ITEMS = 2; const FINAL_ITEMS = 2; export default function Breadcrumb({ note, noteContext }: { note: FNote, noteContext: NoteContext }) { - const notePath = buildNotePaths(noteContext?.notePathArray); + const notePath = buildNotePaths(noteContext); return (
@@ -187,14 +187,27 @@ function BreadcrumbCollapsed({ items, noteContext }: { items: string[], noteCont ); } -function buildNotePaths(notePathArray: string[] | undefined) { +function buildNotePaths(noteContext: NoteContext) { + const notePathArray = noteContext.notePathArray; if (!notePathArray) return []; let prefix = ""; - const output: string[] = []; + let output: string[] = []; + let pos = 0; + let hoistedNotePos = -1; for (const notePath of notePathArray) { + if (noteContext.hoistedNoteId !== "root" && notePath === noteContext.hoistedNoteId) { + hoistedNotePos = pos; + } output.push(`${prefix}${notePath}`); prefix += `${notePath}/`; + pos++; } + + // When hoisted, display only the path starting with the hoisted note. + if (noteContext.hoistedNoteId !== "root") { + output = output.slice(hoistedNotePos); + } + return output; } From 80b61a35a96b02e0941d0cef23ff2654b058d166 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 15 Dec 2025 13:25:21 +0200 Subject: [PATCH 02/17] feat(breadcrumb): display correct icon for first note when hoisted --- apps/client/src/widgets/layout/Breadcrumb.tsx | 6 +- apps/client/src/widgets/react/hooks.tsx | 104 ++++++++++++------ 2 files changed, 74 insertions(+), 36 deletions(-) diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index 085b92ef7..7278b403b 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -11,7 +11,7 @@ import froca from "../../services/froca"; import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { FormListItem } from "../react/FormList"; -import { useChildNotes, useNoteLabel, useNoteProperty } from "../react/hooks"; +import { useChildNotes, useNote, useNoteLabel, useNoteProperty } from "../react/hooks"; import Icon from "../react/Icon"; import NoteLink from "../react/NoteLink"; @@ -57,7 +57,7 @@ export default function Breadcrumb({ note, noteContext }: { note: FNote, noteCon } function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined }) { - const note = useMemo(() => froca.getNoteFromCache("root"), []); + const note = useNote(noteContext?.hoistedNoteId); useNoteLabel(note, "iconClass"); const title = useNoteProperty(note, "title"); @@ -66,7 +66,7 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined className="root-note" icon={note.getIcon()} text={title ?? ""} - onClick={() => noteContext?.setNote("root")} + onClick={() => noteContext?.setNote(note.noteId)} onContextMenu={(e) => { e.preventDefault(); link_context_menu.openContextMenu(note.noteId, e); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 0b9546694..26b699e1e 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -1,29 +1,30 @@ -import { CSSProperties } from "preact/compat"; -import { DragData } from "../note_tree"; import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons"; -import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; -import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils"; -import { RefObject, VNode } from "preact"; import { Tooltip } from "bootstrap"; -import { ViewMode, ViewScope } from "../../services/link"; +import Mark from "mark.js"; +import { RefObject, VNode } from "preact"; +import { CSSProperties } from "preact/compat"; +import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; + import appContext, { EventData, EventNames } from "../../components/app_context"; -import attributes from "../../services/attributes"; -import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; import Component from "../../components/component"; +import NoteContext from "../../components/note_context"; import FBlob from "../../entities/fblob"; import FNote from "../../entities/fnote"; +import attributes from "../../services/attributes"; +import froca from "../../services/froca"; 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 { ViewScope } from "../../services/link"; import options, { type OptionValue } from "../../services/options"; import protected_session_holder from "../../services/protected_session_holder"; +import server from "../../services/server"; +import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts"; import SpacedUpdate from "../../services/spaced_update"; import toast, { ToastOptions } from "../../services/toast"; import utils, { escapeRegExp, randomString, reloadFrontendApp } from "../../services/utils"; -import server from "../../services/server"; -import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts"; -import froca from "../../services/froca"; +import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; +import NoteContextAwareWidget from "../note_context_aware_widget"; +import { DragData } from "../note_tree"; +import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { const parentComponent = useContext(ParentComponent); @@ -42,7 +43,7 @@ export function useTriliumEvents(eventNames: T[], handler: for (const eventName of eventNames) { handlers.push({ eventName, callback: (data) => { handler(data, eventName); - }}) + }}); } for (const { eventName, callback } of handlers) { @@ -111,8 +112,8 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId); dataSaved?.(data); - } - }, [ note, getData, dataSaved ]) + }; + }, [ note, getData, dataSaved ]); const spacedUpdate = useSpacedUpdate(callback); // React to note/blob changes. @@ -137,7 +138,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => { if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return; await spacedUpdate.updateNowIfNecessary(); - }) + }); // Save if needed upon window/browser closing. useEffect(() => { @@ -170,7 +171,7 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st if (needsRefresh) { reloadFrontendApp(`option change: ${name}`); } - } + }; }, [ name, needsRefresh ]); useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => { @@ -178,14 +179,14 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st const newValue = options.get(name); setValue(newValue); } - }, [ name, setValue ])); + }, [ name, setValue ])); useDebugValue(name); return [ value, wrappedSetValue - ] + ]; } /** @@ -266,7 +267,7 @@ export function useTriliumOptions(...names: T[]) { * @returns a name with the given prefix and a random alpanumeric string appended to it. */ export function useUniqueName(prefix?: string) { - return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]); + return useMemo(() => (prefix ? `${prefix }-` : "") + utils.randomString(10), [ prefix ]); } export function useNoteContext() { @@ -428,7 +429,7 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: Re const setter = useCallback((value: string | undefined) => { if (note) { - attributes.setAttribute(note, "relation", relationName, value) + attributes.setAttribute(note, "relation", relationName, value); } }, [note]); @@ -478,7 +479,7 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: FilterLa const setter = useCallback((value: string | null | undefined) => { if (note) { if (value !== null) { - attributes.setLabel(note.noteId, labelName, value) + attributes.setLabel(note.noteId, labelName, value); } else { attributes.removeOwnedLabelByName(note, labelName); } @@ -537,7 +538,7 @@ export function useNoteLabelInt(note: FNote | undefined | null, labelName: Filte return [ (value ? parseInt(value, 10) : undefined), (newValue) => setValue(String(newValue)) - ] + ]; } export function useNoteBlob(note: FNote | null | undefined, componentId?: string): FBlob | null | undefined { @@ -554,7 +555,7 @@ export function useNoteBlob(note: FNote | null | undefined, componentId?: string } } - useEffect(() => { refresh() }, [ note?.noteId ]); + useEffect(() => { refresh(); }, [ note?.noteId ]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (!note) return; @@ -616,7 +617,7 @@ export function useLegacyWidget(widgetFactory: () => T, { useDebugValue(widget); - return [
, widget ] + return [
, widget ]; } /** @@ -643,7 +644,7 @@ export function useElementSize(ref: RefObject) { return () => { resizeObserver.unobserve(element); resizeObserver.disconnect(); - } + }; }, [ ref ]); return size; @@ -703,7 +704,7 @@ export function useTooltip(elRef: RefObject, config: Partial(); +const tooltips = new Set(); /** * Similar to {@link useTooltip}, but doesn't expose methods to imperatively hide or show the tooltip. @@ -732,7 +733,7 @@ export function useStaticTooltip(elRef: RefObject, config?: Partial { if (!highlightedTokens?.length) return null; const regex = highlightedTokens.map((token) => escapeRegExp(token)).join("|"); - return new RegExp(regex, "gi") + return new RegExp(regex, "gi"); }, [ highlightedTokens ]); return (el: HTMLElement | null | undefined) => { @@ -842,7 +843,7 @@ export function useNoteTreeDrag(containerRef: MutableRef { container.removeEventListener("dragenter", onDragEnter); @@ -878,7 +879,7 @@ export function useKeyboardShortcuts(scope: "code-detail" | "text-detail", conta for (const binding of bindings) { removeIndividualBinding(binding); } - } + }; }, [ scope, containerRef, parentComponent, ntxId ]); } @@ -986,3 +987,40 @@ export function useLauncherVisibility(launchNoteId: string) { return isVisible; } + +export function useNote(noteId: string | null | undefined, silentNotFoundError = false) { + const [ note, setNote ] = useState(noteId ? froca.getNoteFromCache(noteId) : undefined); + const requestIdRef = useRef(0); + + useEffect(() => { + if (!noteId) { + setNote(undefined); + return; + } + + if (note?.noteId === noteId) { + return; + } + + // Try to read from cache. + const cachedNote = froca.getNoteFromCache(noteId); + if (cachedNote) { + setNote(cachedNote); + return; + } + + // Read it asynchronously. + const requestId = ++requestIdRef.current; + froca.getNote(noteId, silentNotFoundError).then(readNote => { + // Only update if this is the latest request. + if (readNote && requestId === requestIdRef.current) { + setNote(readNote); + } + }); + }, [ note, noteId, silentNotFoundError ]); + + if (note?.noteId === noteId) { + return note; + } + return undefined; +} From 441958028d19f2fdcfff40b29808459d2313a3e2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 15 Dec 2025 14:01:14 +0200 Subject: [PATCH 03/17] feat(breadcrumb): display workspace text --- apps/client/src/widgets/layout/Breadcrumb.css | 8 ++++++++ apps/client/src/widgets/layout/Breadcrumb.tsx | 5 +++-- apps/client/src/widgets/react/Button.tsx | 13 ++++++------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/client/src/widgets/layout/Breadcrumb.css b/apps/client/src/widgets/layout/Breadcrumb.css index d98b1ed91..55d393f34 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.css +++ b/apps/client/src/widgets/layout/Breadcrumb.css @@ -40,6 +40,14 @@ padding: 0; } + .btn.root-note { + box-shadow: unset; + background: unset; + padding-inline: 0.5em; + color: inherit; + min-width: unset; + } + .dropdown-item span, .dropdown-item strong, .breadcrumb-last-item { diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index 7278b403b..e3d6cf419 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -9,6 +9,7 @@ import FNote from "../../entities/fnote"; import link_context_menu from "../../menus/link_context_menu"; import froca from "../../services/froca"; import ActionButton from "../react/ActionButton"; +import Button from "../react/Button"; import Dropdown from "../react/Dropdown"; import { FormListItem } from "../react/FormList"; import { useChildNotes, useNote, useNoteLabel, useNoteProperty } from "../react/hooks"; @@ -62,10 +63,10 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined const title = useNoteProperty(note, "title"); return (note && - noteContext?.setNote(note.noteId)} onContextMenu={(e) => { e.preventDefault(); diff --git a/apps/client/src/widgets/react/Button.tsx b/apps/client/src/widgets/react/Button.tsx index 6089f1709..8ba5bd465 100644 --- a/apps/client/src/widgets/react/Button.tsx +++ b/apps/client/src/widgets/react/Button.tsx @@ -1,11 +1,11 @@ -import type { ComponentChildren, RefObject } from "preact"; -import type { CSSProperties } from "preact/compat"; -import { useMemo } from "preact/hooks"; +import type { ComponentChildren, HTMLAttributes, RefObject } from "preact"; import { memo } from "preact/compat"; +import { useMemo } from "preact/hooks"; + import { CommandNames } from "../../components/app_context"; import Icon from "./Icon"; -export interface ButtonProps { +export interface ButtonProps extends Pick, "className" | "style" | "onContextMenu"> { name?: string; /** Reference to the button element. Mostly useful for requesting focus. */ buttonRef?: RefObject; @@ -18,7 +18,6 @@ export interface ButtonProps { primary?: boolean; disabled?: boolean; size?: "normal" | "small" | "micro"; - style?: CSSProperties; triggerCommand?: CommandNames; title?: string; } @@ -78,7 +77,7 @@ export function ButtonGroup({ children }: { children: ComponentChildren }) {
{children}
- ) + ); } export function SplitButton({ text, icon, children, ...restProps }: { @@ -103,7 +102,7 @@ export function SplitButton({ text, icon, children, ...restProps }: { {children} - ) + ); } export default Button; From eb99352fff502bf2a4187152ca6c95156b9199b8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 15 Dec 2025 15:29:53 +0200 Subject: [PATCH 04/17] Revert "feat(breadcrumb): display workspace text" This reverts commit 441958028d19f2fdcfff40b29808459d2313a3e2. --- apps/client/src/widgets/layout/Breadcrumb.css | 8 -------- apps/client/src/widgets/layout/Breadcrumb.tsx | 5 ++--- apps/client/src/widgets/react/Button.tsx | 13 +++++++------ 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/apps/client/src/widgets/layout/Breadcrumb.css b/apps/client/src/widgets/layout/Breadcrumb.css index 55d393f34..d98b1ed91 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.css +++ b/apps/client/src/widgets/layout/Breadcrumb.css @@ -40,14 +40,6 @@ padding: 0; } - .btn.root-note { - box-shadow: unset; - background: unset; - padding-inline: 0.5em; - color: inherit; - min-width: unset; - } - .dropdown-item span, .dropdown-item strong, .breadcrumb-last-item { diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index e3d6cf419..7278b403b 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -9,7 +9,6 @@ import FNote from "../../entities/fnote"; import link_context_menu from "../../menus/link_context_menu"; import froca from "../../services/froca"; import ActionButton from "../react/ActionButton"; -import Button from "../react/Button"; import Dropdown from "../react/Dropdown"; import { FormListItem } from "../react/FormList"; import { useChildNotes, useNote, useNoteLabel, useNoteProperty } from "../react/hooks"; @@ -63,10 +62,10 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined const title = useNoteProperty(note, "title"); return (note && -