From e1611d83a3bf817588e097787da50229a45779fe Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 14 Dec 2025 11:12:14 +0200 Subject: [PATCH 01/45] fix(breadcrumb): tree displayed in root navigation --- apps/client/src/widgets/layout/Breadcrumb.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index dd704a0c9..5f0397c7e 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -139,6 +139,8 @@ function BreadcrumbSeparatorDropdownContent({ notePath, noteContext, activeNoteP return ( + + { + dropdownRef.current?.hide(); + showSimilarNotes(); + }} + /> ); } @@ -228,6 +246,17 @@ function NoteInfoValue({ text, title, value }: { text: string; title?: string, v ); } + +function SimilarNotesPane({ note, shown }: StatusBarContext & { + shown: boolean; + setShown: (value: boolean) => void; +}) { + return (shown && +
+ +
+ ); +} //#endregion //#region Backlinks diff --git a/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx b/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx index e324c5d1d..c8dc1337f 100644 --- a/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx +++ b/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx @@ -1,12 +1,13 @@ -import { useEffect, useState } from "preact/hooks"; -import { TabContext } from "./ribbon-interface"; import { SimilarNoteResponse } from "@triliumnext/commons"; -import server from "../../services/server"; -import { t } from "../../services/i18n"; -import froca from "../../services/froca"; -import NoteLink from "../react/NoteLink"; +import { useEffect, useState } from "preact/hooks"; -export default function SimilarNotesTab({ note }: TabContext) { +import froca from "../../services/froca"; +import { t } from "../../services/i18n"; +import server from "../../services/server"; +import NoteLink from "../react/NoteLink"; +import { TabContext } from "./ribbon-interface"; + +export default function SimilarNotesTab({ note }: Pick) { const [ similarNotes, setSimilarNotes ] = useState(); useEffect(() => { @@ -17,7 +18,7 @@ export default function SimilarNotesTab({ note }: TabContext) { await froca.getNotes(noteIds, true); // preload all at once } setSimilarNotes(similarNotes); - }); + }); } }, [ note?.noteId ]); @@ -42,5 +43,5 @@ export default function SimilarNotesTab({ note }: TabContext) { )} - ) -} \ No newline at end of file + ); +} From 749074ea94711eeb54bc93a368420749e85ddcbd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 14 Dec 2025 23:35:16 +0200 Subject: [PATCH 32/45] chore(layout/status_bar): enforce single pane opened at a time --- apps/client/src/widgets/layout/StatusBar.tsx | 41 ++++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 6a154730a..e4724b31c 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -17,7 +17,6 @@ import server from "../../services/server"; import { openInAppHelpFromUrl } from "../../services/utils"; import { formatDateTime } from "../../utils/formatters"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; -import Collapsible from "../react/Collapsible"; import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useActiveNoteContext, useLegacyImperativeHandlers, useNoteLabel, useNoteProperty, useStaticTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks"; @@ -44,19 +43,27 @@ interface StatusBarContext { export default function StatusBar() { const { note, notePath, noteContext, viewScope, hoistedNoteId } = useActiveNoteContext(); - const [ attributesShown, setAttributesShown ] = useState(false); - const [ similarNotesShown, setSimilarNotesShown ] = useState(false); + const [ activePane, setActivePane ] = useState<"attributes" | "similar-notes" | null>(null); const context: StatusBarContext | undefined | null = note && noteContext && { note, notePath, noteContext, viewScope, hoistedNoteId }; - const attributesContext: AttributesProps | undefined | null = context && { ...context, attributesShown, setAttributesShown }; + const attributesContext: AttributesProps | undefined | null = context && { + ...context, + attributesShown: activePane === "attributes", + setAttributesShown: () => setActivePane("attributes") + }; + const noteInfoContext: NoteInfoContext | undefined | null = context && { + ...context, + similarNotesShown: activePane === "similar-notes", + setSimilarNotesShown: () => setActivePane("similar-notes") + }; const isHiddenNote = note?.isInHiddenSubtree(); return (
{attributesContext && } - {context && } + {noteInfoContext && }
- {context && attributesContext && <> + {context && attributesContext && noteInfoContext && <>
@@ -66,7 +73,7 @@ export default function StatusBar() { - setSimilarNotesShown(true)} /> +
}
@@ -202,10 +209,13 @@ export function getLocaleName(locale: Locale | null | undefined) { } //#endregion -//#region Note info -export function NoteInfoBadge({ note, showSimilarNotes }: StatusBarContext & { - showSimilarNotes: () => void -}) { +//#region Note info & Similar +interface NoteInfoContext extends StatusBarContext { + similarNotesShown: boolean; + setSimilarNotesShown: (value: boolean) => void; +} + +export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) { const dropdownRef = useRef(null); const { metadata, ...sizeProps } = useNoteMetadata(note); const [ originalFileName ] = useNoteLabel(note, "originalFileName"); @@ -231,7 +241,7 @@ export function NoteInfoBadge({ note, showSimilarNotes }: StatusBarContext & { text={t("note_info_widget.show_similar_notes")} onClick={() => { dropdownRef.current?.hide(); - showSimilarNotes(); + setSimilarNotesShown(true); }} /> @@ -247,11 +257,8 @@ function NoteInfoValue({ text, title, value }: { text: string; title?: string, v ); } -function SimilarNotesPane({ note, shown }: StatusBarContext & { - shown: boolean; - setShown: (value: boolean) => void; -}) { - return (shown && +function SimilarNotesPane({ note, similarNotesShown }: NoteInfoContext) { + return (similarNotesShown &&
From 8fa6e3838268a07ba78470d7039032c7c8d8dc3f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 14 Dec 2025 23:50:40 +0200 Subject: [PATCH 33/45] refactor(ribbon): decouple completely from new layout --- apps/client/src/layouts/desktop_layout.tsx | 3 +- apps/client/src/widgets/ribbon/Ribbon.tsx | 13 +++---- .../src/widgets/ribbon/RibbonDefinition.ts | 36 +++++++++---------- .../src/widgets/ribbon/ribbon-interface.ts | 4 +-- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index b443bd27d..f4ac06e87 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -143,8 +143,7 @@ export default class DesktopLayout { .optChild(!isNewLayout, ) .optChild(!isNewLayout, ) .optChild(isNewLayout, )) - .optChild(!isNewLayout, ) - .optChild(isNewLayout, ) + .optChild(!isNewLayout, ) .child(new WatchedFileUpdateStatusWidget()) .child() .child( diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index c8352ee02..411c7de21 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -5,9 +5,9 @@ import clsx from "clsx"; import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { EventNames } from "../../components/app_context"; -import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import { Indexed, numberObjectsInPlace } from "../../services/utils"; import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks"; +import NoteActions from "./NoteActions"; import { TabConfiguration, TitleContext } from "./ribbon-interface"; import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition"; @@ -17,9 +17,7 @@ interface ComputedTab extends Indexed { shouldShow: boolean; } -const isNewLayout = isExperimentalFeatureEnabled("new-layout"); - -export default function Ribbon({ children }: { children?: preact.ComponentChildren }) { +export default function Ribbon() { const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext(); const noteType = useNoteProperty(note, "type"); const [ activeTabIndex, setActiveTabIndex ] = useState(); @@ -32,8 +30,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr async function refresh() { const computedTabs: ComputedTab[] = []; for (const tab of TAB_CONFIGURATION) { - const shouldAvoid = (isNewLayout && tab.avoidInNewLayout); - const shouldShow = !shouldAvoid && await shouldShowTab(tab.show, titleContext); + const shouldShow = await shouldShowTab(tab.show, titleContext); computedTabs.push({ ...tab, shouldShow: !!shouldShow @@ -92,7 +89,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr /> ))}
- {children} +
@@ -115,7 +112,7 @@ export default function Ribbon({ children }: { children?: preact.ComponentChildr noteContext={noteContext} componentId={componentId} activate={useCallback(() => { - setActiveTabIndex(tab.index) + setActiveTabIndex(tab.index); }, [setActiveTabIndex])} />
diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts index 86f8edde0..2adf748b3 100644 --- a/apps/client/src/widgets/ribbon/RibbonDefinition.ts +++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts @@ -1,4 +1,3 @@ -import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import { t } from "../../services/i18n"; import options from "../../services/options"; import BasicPropertiesTab from "./BasicPropertiesTab"; @@ -18,8 +17,6 @@ import ScriptTab from "./ScriptTab"; import SearchDefinitionTab from "./SearchDefinitionTab"; import SimilarNotesTab from "./SimilarNotesTab"; -const isNewLayout = isExperimentalFeatureEnabled("new-layout"); - export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ { title: t("classic_editor_toolbar.title"), @@ -30,15 +27,14 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ toggleCommand: "toggleRibbonTabClassicEditor", content: FormattingToolbar, activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"), - stayInDom: !isNewLayout, - avoidInNewLayout: true + stayInDom: true }, { title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"), icon: "bx bx-play", content: ScriptTab, activate: true, - show: ({ note }) => note && !isNewLayout && + show: ({ note }) => note && (note.isTriliumScript() || note.isTriliumSqlite()) && (note.hasLabel("executeDescription") || note.hasLabel("executeButton")) }, @@ -47,34 +43,34 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ icon: "bx bx-search", content: SearchDefinitionTab, activate: true, - show: ({ note }) => !isNewLayout && note?.type === "search" + show: ({ note }) => note?.type === "search" }, { title: t("edited_notes.title"), icon: "bx bx-calendar-edit", content: EditedNotesTab, - show: ({ note }) => !isNewLayout && note?.hasOwnedLabel("dateNote"), + show: ({ note }) => note?.hasOwnedLabel("dateNote"), activate: () => options.is("editedNotesOpenInRibbon") }, { title: t("book_properties.book_properties"), icon: "bx bx-book", content: CollectionPropertiesTab, - show: ({ note }) => !isNewLayout && (note?.type === "book" || note?.type === "search"), + show: ({ note }) => (note?.type === "book" || note?.type === "search"), toggleCommand: "toggleRibbonTabBookProperties" }, { title: t("note_properties.info"), icon: "bx bx-info-square", content: NotePropertiesTab, - show: ({ note }) => !isNewLayout && !!note?.getLabelValue("pageUrl"), + show: ({ note }) => !!note?.getLabelValue("pageUrl"), activate: true }, { title: t("file_properties.title"), icon: "bx bx-file", content: FilePropertiesTab, - show: ({ note }) => !isNewLayout && note?.type === "file", + show: ({ note }) => note?.type === "file", toggleCommand: "toggleRibbonTabFileProperties", activate: ({ note }) => note?.mime !== "application/pdf" }, @@ -82,7 +78,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ title: t("image_properties.title"), icon: "bx bx-image", content: ImagePropertiesTab, - show: ({ note }) => !isNewLayout && note?.type === "image", + show: ({ note }) => note?.type === "image", toggleCommand: "toggleRibbonTabImageProperties", activate: true, }, @@ -90,49 +86,49 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ title: t("basic_properties.basic_properties"), icon: "bx bx-slider", content: BasicPropertiesTab, - show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), + show: ({note}) => !note?.isLaunchBarConfig(), toggleCommand: "toggleRibbonTabBasicProperties" }, { title: t("owned_attribute_list.owned_attributes"), icon: "bx bx-list-check", content: OwnedAttributesTab, - show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), + show: ({note}) => !note?.isLaunchBarConfig(), toggleCommand: "toggleRibbonTabOwnedAttributes", - stayInDom: !isNewLayout + stayInDom: true }, { title: t("inherited_attribute_list.title"), icon: "bx bx-list-plus", content: InheritedAttributesTab, - show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), + show: ({note}) => !note?.isLaunchBarConfig(), toggleCommand: "toggleRibbonTabInheritedAttributes" }, { title: t("note_paths.title"), icon: "bx bx-collection", content: NotePathsTab, - show: !isNewLayout, + show: true, toggleCommand: "toggleRibbonTabNotePaths" }, { title: t("note_map.title"), icon: "bx bxs-network-chart", content: NoteMapTab, - show: !isNewLayout, + show: true, toggleCommand: "toggleRibbonTabNoteMap" }, { title: t("similar_notes.title"), icon: "bx bx-bar-chart", - show: ({ note }) => !isNewLayout && note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"), + show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"), content: SimilarNotesTab, toggleCommand: "toggleRibbonTabSimilarNotes" }, { title: t("note_info_widget.title"), icon: "bx bx-info-circle", - show: ({ note }) => !isNewLayout && !!note, + show: ({ note }) => !!note, content: NoteInfoTab, toggleCommand: "toggleRibbonTabNoteInfo" } diff --git a/apps/client/src/widgets/ribbon/ribbon-interface.ts b/apps/client/src/widgets/ribbon/ribbon-interface.ts index a83bbc55f..77543994d 100644 --- a/apps/client/src/widgets/ribbon/ribbon-interface.ts +++ b/apps/client/src/widgets/ribbon/ribbon-interface.ts @@ -1,7 +1,8 @@ import { KeyboardActionNames } from "@triliumnext/commons"; +import { VNode } from "preact"; + import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; -import { VNode } from "preact"; export interface TabContext { note: FNote | null | undefined; @@ -30,5 +31,4 @@ export interface TabConfiguration { * By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed. */ stayInDom?: boolean; - avoidInNewLayout?: boolean; } From d1ae2db587a70064f3bf882f6f4f35ed6e5671bb Mon Sep 17 00:00:00 2001 From: Adorian Doran Date: Mon, 15 Dec 2025 00:31:36 +0200 Subject: [PATCH 34/45] client/status bar/note paths: replace the "Clone note to new location" button with a link --- apps/client/src/widgets/react/LinkButton.tsx | 16 ++++++++++------ apps/client/src/widgets/ribbon/NotePathsTab.tsx | 7 ++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/client/src/widgets/react/LinkButton.tsx b/apps/client/src/widgets/react/LinkButton.tsx index a9596c972..81e2943a1 100644 --- a/apps/client/src/widgets/react/LinkButton.tsx +++ b/apps/client/src/widgets/react/LinkButton.tsx @@ -1,16 +1,20 @@ import { ComponentChild } from "preact"; +import { CommandNames } from "../../components/app_context"; interface LinkButtonProps { - onClick: () => void; + onClick?: () => void; text: ComponentChild; + triggerCommand?: CommandNames; } -export default function LinkButton({ onClick, text }: LinkButtonProps) { +export default function LinkButton({ onClick, text, triggerCommand }: LinkButtonProps) { return ( - { - e.preventDefault(); - onClick(); - }}> + { + e.preventDefault(); + if (onClick) onClick(); + }}> {text} ) diff --git a/apps/client/src/widgets/ribbon/NotePathsTab.tsx b/apps/client/src/widgets/ribbon/NotePathsTab.tsx index d639a8928..18d5086d6 100644 --- a/apps/client/src/widgets/ribbon/NotePathsTab.tsx +++ b/apps/client/src/widgets/ribbon/NotePathsTab.tsx @@ -3,11 +3,12 @@ import { useEffect, useMemo, useState } from "preact/hooks"; import FNote, { NotePathRecord } from "../../entities/fnote"; import { t } from "../../services/i18n"; import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree"; -import Button from "../react/Button"; import { useTriliumEvent } from "../react/hooks"; import NoteLink from "../react/NoteLink"; import { joinElements } from "../react/react_utils"; import { TabContext } from "./ribbon-interface"; +import LinkButton from "../react/LinkButton"; + export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) { const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); @@ -35,9 +36,9 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: { )) : undefined} -