From 532df6559a0bf2d067c1fccc464486e452b239f3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 9 Nov 2025 20:03:43 +0200 Subject: [PATCH] fix(ribbon): formatting toolbar displayed in read-only notes --- apps/client/src/services/utils.ts | 2 +- apps/client/src/widgets/ribbon/Ribbon.tsx | 47 ++++++++++++++----- .../src/widgets/ribbon/RibbonDefinition.ts | 4 +- .../src/widgets/ribbon/ribbon-interface.ts | 3 +- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index f5e037be5..b5b21dfe0 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -841,7 +841,7 @@ export function arrayEqual(a: T[], b: T[]) { return true; } -type Indexed = T & { index: number }; +export type Indexed = T & { index: number }; /** * Given an object array, alters every object in the array to have an index field assigned to it. diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index 1c5b8a7bb..f14e03367 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -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 { numberObjectsInPlace } from "../../services/utils"; +import { Indexed, numberObjectsInPlace } from "../../services/utils"; import { EventNames } from "../../components/app_context"; import NoteActions from "./NoteActions"; import { KeyboardActionNames } from "@triliumnext/commons"; @@ -11,23 +11,39 @@ import { TabConfiguration, TitleContext } from "./ribbon-interface"; const TAB_CONFIGURATION = numberObjectsInPlace(RIBBON_TAB_DEFINITIONS); +interface ComputedTab extends Indexed { + shouldShow: boolean; +} + export default function Ribbon() { const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext(); const noteType = useNoteProperty(note, "type"); - const titleContext: TitleContext = { note }; const [ activeTabIndex, setActiveTabIndex ] = useState(); - const computedTabs = useMemo( - () => TAB_CONFIGURATION.map(tab => { - const shouldShow = typeof tab.show === "boolean" ? tab.show : tab.show?.(titleContext); - return { + const [ computedTabs, setComputedTabs ] = useState(); + 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({ ...tab, - shouldShow - } - }), - [ titleContext, note, noteType ]); + shouldShow: !!shouldShow + }); + } + setComputedTabs(computedTabs); + } + + useEffect(() => { + refresh(); + }, [ 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); }, [ note?.noteId ]); @@ -35,6 +51,7 @@ export default function Ribbon() { // 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) { @@ -51,7 +68,7 @@ export default function Ribbon() { <>
- {computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => ( + {computedTabs && computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => ( shouldShow &&
- {computedTabs.map(tab => { + {computedTabs && computedTabs.map(tab => { const isActive = tab.index === activeTabIndex; if (!isActive && !tab.stayInDom) { return; @@ -129,3 +146,9 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri ) } +async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise | 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); +} diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts index 8f37053dc..9f19707cb 100644 --- a/apps/client/src/widgets/ribbon/RibbonDefinition.ts +++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts @@ -21,7 +21,9 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ { title: t("classic_editor_toolbar.title"), icon: "bx bx-text", - show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic", + show: async ({ note, noteContext }) => note?.type === "text" + && options.get("textNoteEditorType") === "ckeditor-classic" + && !(await noteContext?.isReadOnly()), toggleCommand: "toggleRibbonTabClassicEditor", content: FormattingToolbar, activate: true, diff --git a/apps/client/src/widgets/ribbon/ribbon-interface.ts b/apps/client/src/widgets/ribbon/ribbon-interface.ts index 2fbc40612..7ab982dd2 100644 --- a/apps/client/src/widgets/ribbon/ribbon-interface.ts +++ b/apps/client/src/widgets/ribbon/ribbon-interface.ts @@ -16,13 +16,14 @@ 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) => boolean | null | undefined); + show: boolean | ((context: TitleContext) => Promise | boolean | null | undefined); toggleCommand?: KeyboardActionNames; activate?: boolean | ((context: TitleContext) => boolean); /**