From c8888261925195101387099094b1e7ca9b196823 Mon Sep 17 00:00:00 2001 From: Zaki Ur Rehman Date: Thu, 16 Apr 2026 18:06:14 +0500 Subject: [PATCH 1/4] fex: UI overlap in attribute editing --- .../ribbon/components/AttributeEditor.tsx | 495 +++++++++++------- 1 file changed, 315 insertions(+), 180 deletions(-) diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx index edae4f014f..2c694c4e70 100644 --- a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -1,19 +1,39 @@ -import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5"; +import { + AttributeEditor as CKEditorAttributeEditor, + MentionFeed, + ModelElement, + ModelNode, + ModelPosition, +} from "@triliumnext/ckeditor5"; import { AttributeType } from "@triliumnext/commons"; import { createPortal } from "preact/compat"; -import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/hooks"; +import { + MutableRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "preact/hooks"; -import type { CommandData, FilteredCommandNames } from "../../../components/app_context"; +import type { + CommandData, + FilteredCommandNames, +} from "../../../components/app_context"; import FAttribute from "../../../entities/fattribute"; import FNote from "../../../entities/fnote"; import contextMenu from "../../../menus/context_menu"; -import attribute_parser, { Attribute } from "../../../services/attribute_parser"; +import attribute_parser, { + Attribute, +} from "../../../services/attribute_parser"; import attribute_renderer from "../../../services/attribute_renderer"; import attributes from "../../../services/attributes"; import froca from "../../../services/froca"; import { t } from "../../../services/i18n"; import link from "../../../services/link"; -import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete"; +import note_autocomplete, { + Suggestion, +} from "../../../services/note_autocomplete"; import note_create from "../../../services/note_create"; import server from "../../../services/server"; import { isIMEComposing } from "../../../services/shortcuts"; @@ -21,7 +41,13 @@ import { escapeQuotes, getErrorMessage } from "../../../services/utils"; import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; import ActionButton from "../../react/ActionButton"; import CKEditor, { CKEditorApi } from "../../react/CKEditor"; -import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks"; +import { + useLegacyImperativeHandlers, + useLegacyWidget, + useTooltip, + useTriliumEvent, + useTriliumOption, +} from "../../react/hooks"; type AttributeCommandNames = FilteredCommandNames; @@ -35,7 +61,8 @@ const HELP_TEXT = ` const mentionSetup: MentionFeed[] = [ { marker: "@", - feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText), + feed: (queryText) => + note_autocomplete.autocompleteSourceForCKEditor(queryText), itemRenderer: (_item) => { const item = _item as Suggestion; const itemElement = document.createElement("button"); @@ -44,39 +71,42 @@ const mentionSetup: MentionFeed[] = [ return itemElement; }, - minimumCharacters: 0 + minimumCharacters: 0, }, { marker: "#", feed: async (queryText) => { - const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); + const names = await server.get( + `attribute-names/?type=label&query=${encodeURIComponent(queryText)}`, + ); return names.map((name) => { return { id: `#${name}`, - name + name, }; }); }, - minimumCharacters: 0 + minimumCharacters: 0, }, { marker: "~", feed: async (queryText) => { - const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); + const names = await server.get( + `attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`, + ); return names.map((name) => { return { id: `~${name}`, - name + name, }; }); }, - minimumCharacters: 0 - } + minimumCharacters: 0, + }, ]; - interface AttributeEditorProps { api: MutableRef; note: FNote; @@ -93,27 +123,38 @@ export interface AttributeEditorImperativeHandlers { renderOwnedAttributes(ownedAttributes: FAttribute[]): Promise; } -export default function AttributeEditor({ api, note, componentId, notePath, ntxId, hidden }: AttributeEditorProps) { - const [ currentValue, setCurrentValue ] = useState(""); - const [ state, setState ] = useState<"normal" | "showHelpTooltip" | "showAttributeDetail">(); - const [ error, setError ] = useState(); - const [ needsSaving, setNeedsSaving ] = useState(false); - +export default function AttributeEditor({ + api, + note, + componentId, + notePath, + ntxId, + hidden, +}: AttributeEditorProps) { + const [currentValue, setCurrentValue] = useState(""); + const [state, setState] = useState< + "normal" | "showHelpTooltip" | "showAttributeDetail" + >(); + const [error, setError] = useState(); + const [needsSaving, setNeedsSaving] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); const lastSavedContent = useRef(); const currentValueRef = useRef(currentValue); const wrapperRef = useRef(null); const editorRef = useRef(); - const [ locale ] = useTriliumOption("locale"); + const [locale] = useTriliumOption("locale"); const { showTooltip, hideTooltip } = useTooltip(wrapperRef, { trigger: "focus", html: true, title: HELP_TEXT, placement: "bottom", - offset: "0,30" + offset: "0,30", }); - const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget()); + const [attributeDetailWidgetEl, attributeDetailWidget] = useLegacyWidget( + () => new AttributeDetailWidget(), + ); useEffect(() => { if (state === "showHelpTooltip") { @@ -121,13 +162,16 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI } else { hideTooltip(); } - }, [ state ]); + }, [state]); - async function renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) { + async function renderOwnedAttributes( + ownedAttributes: FAttribute[], + saved: boolean, + ) { // attrs are not resorted if position changes after the initial load ownedAttributes.sort((a, b) => a.position - b.position); - let htmlAttrs = (`

${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}

`); + let htmlAttrs = `

${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}

`; if (saved) { lastSavedContent.current = htmlAttrs; @@ -144,7 +188,9 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI function parseAttributes() { try { - return attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current)); + return attribute_parser.lexAndParse( + getPreprocessedData(currentValueRef.current), + ); } catch (e: unknown) { setError(e); } @@ -157,7 +203,11 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI return; } - await server.put(`notes/${note.noteId}/attributes`, attributes, componentId); + await server.put( + `notes/${note.noteId}/attributes`, + attributes, + componentId, + ); setNeedsSaving(false); // blink the attribute text to give a visual hint that save has been executed @@ -171,7 +221,9 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI } } - async function handleAddNewAttributeCommand(command: AttributeCommandNames | undefined) { + async function handleAddNewAttributeCommand( + command: AttributeCommandNames | undefined, + ) { // TODO: Not sure what the relation between FAttribute[] and Attribute[] is. const attrs = parseAttributes() as FAttribute[]; @@ -208,7 +260,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI type, name, value, - isInheritable: false + isInheritable: false, }); await renderOwnedAttributes(attrs, false); @@ -224,7 +276,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI isOwned: true, x: rect ? (rect.left + rect.right) / 2 : 0, y: rect?.bottom ?? 0, - focus: "name" + focus: "name", }); }, 100); } @@ -234,34 +286,51 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI renderOwnedAttributes(note.getOwnedAttributes(), true); } - useEffect(() => refresh(), [ note ]); + useEffect(() => refresh(), [note]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { - if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) { + if ( + loadResults + .getAttributeRows(componentId) + .find((attr) => attributes.isAffecting(attr, note)) + ) { refresh(); } }); // Interaction with CKEditor. - useLegacyImperativeHandlers(useMemo(() => ({ - loadReferenceLinkTitle: async ($el: JQuery, href: string) => { - const { noteId } = link.parseNavigationStateFromUrl(href); - const note = noteId ? await froca.getNote(noteId, true) : null; - const title = note ? note.title : "[missing]"; + useLegacyImperativeHandlers( + useMemo( + () => ({ + loadReferenceLinkTitle: async ( + $el: JQuery, + href: string, + ) => { + const { noteId } = link.parseNavigationStateFromUrl(href); + const note = noteId + ? await froca.getNote(noteId, true) + : null; + const title = note ? note.title : "[missing]"; - $el.text(title); - }, - createNoteForReferenceLink: async (title: string) => { - let result; - if (notePath) { - result = await note_create.createNoteWithTypePrompt(notePath, { - activate: false, - title - }); - } + $el.text(title); + }, + createNoteForReferenceLink: async (title: string) => { + let result; + if (notePath) { + result = await note_create.createNoteWithTypePrompt( + notePath, + { + activate: false, + title, + }, + ); + } - return result?.note?.getBestNotePathString(); - } - }), [ notePath ])); + return result?.note?.getBestNotePathString(); + }, + }), + [notePath], + ), + ); // Keyboard shortcuts useTriliumEvent("addNewLabel", ({ ntxId: eventNtxId }) => { @@ -274,140 +343,206 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI }); // Imperative API - useImperativeHandle(api, () => ({ - save, - refresh, - renderOwnedAttributes: (attributes) => renderOwnedAttributes(attributes as FAttribute[], false), - focus: () => editorRef.current?.focus() - }), [ save, refresh, renderOwnedAttributes ]); + useImperativeHandle( + api, + () => ({ + save, + refresh, + renderOwnedAttributes: (attributes) => + renderOwnedAttributes(attributes as FAttribute[], false), + focus: () => editorRef.current?.focus(), + }), + [save, refresh, renderOwnedAttributes], + ); return ( <> - {!hidden &&
{ - // Skip processing during IME composition - if (isIMEComposing(e)) { - return; - } + {!hidden && ( +
{ + // Skip processing during IME composition + if (isIMEComposing(e)) { + return; + } - if (e.key === "Enter") { - // allow autocomplete to fill the result textarea - setTimeout(() => save(), 100); - } - }} - >
- { - currentValueRef.current = currentValue ?? ""; - - const oldValue = getPreprocessedData(lastSavedContent.current ?? "").trimEnd(); - const newValue = getPreprocessedData(currentValue ?? "").trimEnd(); - setNeedsSaving(oldValue !== newValue); - setError(undefined); - }} - onClick={(e, pos) => { - if (pos && pos.textNode && pos.textNode.data) { - const clickIndex = getClickIndex(pos); - - let parsedAttrs: Attribute[]; - - try { - parsedAttrs = attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current), true); - } catch (e: unknown) { - // the input is incorrect because the user messed up with it and now needs to fix it manually - console.log(e); - return null; - } - - let matchedAttr: Attribute | null = null; - - for (const attr of parsedAttrs) { - if (attr.startIndex !== undefined && clickIndex > attr.startIndex && - attr.endIndex !== undefined && clickIndex <= attr.endIndex) { - matchedAttr = attr; - break; - } - } - - setTimeout(() => { - if (matchedAttr) { - attributeDetailWidget.showAttributeDetail({ - allAttributes: parsedAttrs, - attribute: matchedAttr, - isOwned: true, - x: e.pageX, - y: e.pageY - }); - setState("showAttributeDetail"); - } else { - setState("showHelpTooltip"); - } - }, 100); - } else { - setState("showHelpTooltip"); - } - }} - onKeyDown={() => attributeDetailWidget.hide()} - onBlur={() => save()} - onInitialized={() => editorRef.current?.focus()} - disableNewlines disableSpellcheck - /> - -
- { needsSaving && } - - { - // Prevent automatic hiding of the context menu due to the button being clicked. - e.stopPropagation(); - - contextMenu.show({ - x: e.pageX, - y: e.pageY, - orientation: "left", - items: [ - { title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" }, - { title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" }, - { kind: "separator" }, - { title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" }, - { title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" } - ], - selectMenuItemHandler: (item) => handleAddNewAttributeCommand(item.command) - }); + if (e.key === "Enter") { + // allow autocomplete to fill the result textarea + setTimeout(() => save(), 100); + } + }} + > + {" "} +
+ -
-
+ onChange={(currentValue) => { + currentValueRef.current = currentValue ?? ""; - { error && ( -
- {getErrorMessage(error)} + const oldValue = getPreprocessedData( + lastSavedContent.current ?? "", + ).trimEnd(); + const newValue = getPreprocessedData( + currentValue ?? "", + ).trimEnd(); + setNeedsSaving(oldValue !== newValue); + setError(undefined); + }} + onClick={(e, pos) => { + if (pos && pos.textNode && pos.textNode.data) { + const clickIndex = getClickIndex(pos); + + let parsedAttrs: Attribute[]; + + try { + parsedAttrs = + attribute_parser.lexAndParse( + getPreprocessedData( + currentValueRef.current, + ), + true, + ); + } catch (e: unknown) { + // the input is incorrect because the user messed up with it and now needs to fix it manually + console.log(e); + return null; + } + + let matchedAttr: Attribute | null = null; + + for (const attr of parsedAttrs) { + if ( + attr.startIndex !== undefined && + clickIndex > attr.startIndex && + attr.endIndex !== undefined && + clickIndex <= attr.endIndex + ) { + matchedAttr = attr; + break; + } + } + + setTimeout(() => { + if (matchedAttr) { + attributeDetailWidget.showAttributeDetail( + { + allAttributes: parsedAttrs, + attribute: matchedAttr, + isOwned: true, + x: e.pageX, + y: e.pageY, + }, + ); + setState("showAttributeDetail"); + } else { + setState("showHelpTooltip"); + } + }, 100); + } else { + setState("showHelpTooltip"); + } + }} + onKeyDown={() => attributeDetailWidget.hide()} + onBlur={() => save()} + onInitialized={() => editorRef.current?.focus()} + disableNewlines + disableSpellcheck + /> + +
+ {needsSaving && ( + + )} + + { + // Prevent automatic hiding of the context menu due to the button being clicked. + e.stopPropagation(); + setIsMenuOpen(true); + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + orientation: "left", + + items: [ + { + title: t( + "attribute_editor.add_new_label", + ), + command: "addNewLabel", + uiIcon: "bx bx-hash", + }, + { + title: t( + "attribute_editor.add_new_relation", + ), + command: "addNewRelation", + uiIcon: "bx bx-transfer", + }, + { kind: "separator" }, + { + title: t( + "attribute_editor.add_new_label_definition", + ), + command: + "addNewLabelDefinition", + uiIcon: "bx bx-empty", + }, + { + title: t( + "attribute_editor.add_new_relation_definition", + ), + command: + "addNewRelationDefinition", + uiIcon: "bx bx-empty", + }, + ], + selectMenuItemHandler: (item) => + handleAddNewAttributeCommand( + item.command, + ), + onHide: () => setIsMenuOpen(false), + }); + }} + /> +
- )} -
} + {error && ( +
+ {getErrorMessage(error)} +
+ )} +
+ )} {createPortal(attributeDetailWidgetEl, document.body)} From 6bc3176251f3a967aba6d40c616090c8462a856f Mon Sep 17 00:00:00 2001 From: Zaki Ur Rehman Date: Thu, 16 Apr 2026 18:29:57 +0500 Subject: [PATCH 2/4] fix: resolve context menu state reset issue causing UI text overlap --- .../widgets/ribbon/components/AttributeEditor.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx index 2c694c4e70..83d27df1ee 100644 --- a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -138,6 +138,7 @@ export default function AttributeEditor({ const [error, setError] = useState(); const [needsSaving, setNeedsSaving] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); + const suppressNextOnHide = useRef(false); const lastSavedContent = useRef(); const currentValueRef = useRef(currentValue); const wrapperRef = useRef(null); @@ -486,6 +487,11 @@ export default function AttributeEditor({ onClick={(e) => { // Prevent automatic hiding of the context menu due to the button being clicked. e.stopPropagation(); + if (isMenuOpen) { + // If we re-show the menu, ContextMenu.show() will call hide() + // and immediately trigger onHide. Suppress that transient hide. + suppressNextOnHide.current = true; + } setIsMenuOpen(true); contextMenu.show({ @@ -530,7 +536,13 @@ export default function AttributeEditor({ handleAddNewAttributeCommand( item.command, ), - onHide: () => setIsMenuOpen(false), + onHide: () => { + if (suppressNextOnHide.current) { + suppressNextOnHide.current = false; + return; + } + setIsMenuOpen(false); + }, }); }} /> From b2f02962fc977f2316890ac54bd7e9fa8cfabb04 Mon Sep 17 00:00:00 2001 From: Zaki Ur Rehman Date: Thu, 16 Apr 2026 22:54:48 +0500 Subject: [PATCH 3/4] refactor: show only modified lines --- .../ribbon/components/AttributeEditor.tsx | 492 ++++++------------ 1 file changed, 171 insertions(+), 321 deletions(-) diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx index 83d27df1ee..dd5eb8895b 100644 --- a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -1,39 +1,19 @@ -import { - AttributeEditor as CKEditorAttributeEditor, - MentionFeed, - ModelElement, - ModelNode, - ModelPosition, -} from "@triliumnext/ckeditor5"; +import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5"; import { AttributeType } from "@triliumnext/commons"; import { createPortal } from "preact/compat"; -import { - MutableRef, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from "preact/hooks"; +import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/hooks"; -import type { - CommandData, - FilteredCommandNames, -} from "../../../components/app_context"; +import type { CommandData, FilteredCommandNames } from "../../../components/app_context"; import FAttribute from "../../../entities/fattribute"; import FNote from "../../../entities/fnote"; import contextMenu from "../../../menus/context_menu"; -import attribute_parser, { - Attribute, -} from "../../../services/attribute_parser"; +import attribute_parser, { Attribute } from "../../../services/attribute_parser"; import attribute_renderer from "../../../services/attribute_renderer"; import attributes from "../../../services/attributes"; import froca from "../../../services/froca"; import { t } from "../../../services/i18n"; import link from "../../../services/link"; -import note_autocomplete, { - Suggestion, -} from "../../../services/note_autocomplete"; +import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete"; import note_create from "../../../services/note_create"; import server from "../../../services/server"; import { isIMEComposing } from "../../../services/shortcuts"; @@ -41,13 +21,7 @@ import { escapeQuotes, getErrorMessage } from "../../../services/utils"; import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; import ActionButton from "../../react/ActionButton"; import CKEditor, { CKEditorApi } from "../../react/CKEditor"; -import { - useLegacyImperativeHandlers, - useLegacyWidget, - useTooltip, - useTriliumEvent, - useTriliumOption, -} from "../../react/hooks"; +import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks"; type AttributeCommandNames = FilteredCommandNames; @@ -61,8 +35,7 @@ const HELP_TEXT = ` const mentionSetup: MentionFeed[] = [ { marker: "@", - feed: (queryText) => - note_autocomplete.autocompleteSourceForCKEditor(queryText), + feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText), itemRenderer: (_item) => { const item = _item as Suggestion; const itemElement = document.createElement("button"); @@ -71,42 +44,39 @@ const mentionSetup: MentionFeed[] = [ return itemElement; }, - minimumCharacters: 0, + minimumCharacters: 0 }, { marker: "#", feed: async (queryText) => { - const names = await server.get( - `attribute-names/?type=label&query=${encodeURIComponent(queryText)}`, - ); + const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); return names.map((name) => { return { id: `#${name}`, - name, + name }; }); }, - minimumCharacters: 0, + minimumCharacters: 0 }, { marker: "~", feed: async (queryText) => { - const names = await server.get( - `attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`, - ); + const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); return names.map((name) => { return { id: `~${name}`, - name, + name }; }); }, - minimumCharacters: 0, - }, + minimumCharacters: 0 + } ]; + interface AttributeEditorProps { api: MutableRef; note: FNote; @@ -123,39 +93,29 @@ export interface AttributeEditorImperativeHandlers { renderOwnedAttributes(ownedAttributes: FAttribute[]): Promise; } -export default function AttributeEditor({ - api, - note, - componentId, - notePath, - ntxId, - hidden, -}: AttributeEditorProps) { - const [currentValue, setCurrentValue] = useState(""); - const [state, setState] = useState< - "normal" | "showHelpTooltip" | "showAttributeDetail" - >(); - const [error, setError] = useState(); - const [needsSaving, setNeedsSaving] = useState(false); +export default function AttributeEditor({ api, note, componentId, notePath, ntxId, hidden }: AttributeEditorProps) { + const [ currentValue, setCurrentValue ] = useState(""); + const [ state, setState ] = useState<"normal" | "showHelpTooltip" | "showAttributeDetail">(); + const [ error, setError ] = useState(); + const [ needsSaving, setNeedsSaving ] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); const suppressNextOnHide = useRef(false); + const lastSavedContent = useRef(); const currentValueRef = useRef(currentValue); const wrapperRef = useRef(null); const editorRef = useRef(); - const [locale] = useTriliumOption("locale"); + const [ locale ] = useTriliumOption("locale"); const { showTooltip, hideTooltip } = useTooltip(wrapperRef, { trigger: "focus", html: true, title: HELP_TEXT, placement: "bottom", - offset: "0,30", + offset: "0,30" }); - const [attributeDetailWidgetEl, attributeDetailWidget] = useLegacyWidget( - () => new AttributeDetailWidget(), - ); + const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget()); useEffect(() => { if (state === "showHelpTooltip") { @@ -163,16 +123,13 @@ export default function AttributeEditor({ } else { hideTooltip(); } - }, [state]); + }, [ state ]); - async function renderOwnedAttributes( - ownedAttributes: FAttribute[], - saved: boolean, - ) { + async function renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) { // attrs are not resorted if position changes after the initial load ownedAttributes.sort((a, b) => a.position - b.position); - let htmlAttrs = `

${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}

`; + let htmlAttrs = (`

${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}

`); if (saved) { lastSavedContent.current = htmlAttrs; @@ -189,9 +146,7 @@ export default function AttributeEditor({ function parseAttributes() { try { - return attribute_parser.lexAndParse( - getPreprocessedData(currentValueRef.current), - ); + return attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current)); } catch (e: unknown) { setError(e); } @@ -204,11 +159,7 @@ export default function AttributeEditor({ return; } - await server.put( - `notes/${note.noteId}/attributes`, - attributes, - componentId, - ); + await server.put(`notes/${note.noteId}/attributes`, attributes, componentId); setNeedsSaving(false); // blink the attribute text to give a visual hint that save has been executed @@ -222,9 +173,7 @@ export default function AttributeEditor({ } } - async function handleAddNewAttributeCommand( - command: AttributeCommandNames | undefined, - ) { + async function handleAddNewAttributeCommand(command: AttributeCommandNames | undefined) { // TODO: Not sure what the relation between FAttribute[] and Attribute[] is. const attrs = parseAttributes() as FAttribute[]; @@ -261,7 +210,7 @@ export default function AttributeEditor({ type, name, value, - isInheritable: false, + isInheritable: false }); await renderOwnedAttributes(attrs, false); @@ -277,7 +226,7 @@ export default function AttributeEditor({ isOwned: true, x: rect ? (rect.left + rect.right) / 2 : 0, y: rect?.bottom ?? 0, - focus: "name", + focus: "name" }); }, 100); } @@ -287,51 +236,34 @@ export default function AttributeEditor({ renderOwnedAttributes(note.getOwnedAttributes(), true); } - useEffect(() => refresh(), [note]); + useEffect(() => refresh(), [ note ]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { - if ( - loadResults - .getAttributeRows(componentId) - .find((attr) => attributes.isAffecting(attr, note)) - ) { + if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) { refresh(); } }); // Interaction with CKEditor. - useLegacyImperativeHandlers( - useMemo( - () => ({ - loadReferenceLinkTitle: async ( - $el: JQuery, - href: string, - ) => { - const { noteId } = link.parseNavigationStateFromUrl(href); - const note = noteId - ? await froca.getNote(noteId, true) - : null; - const title = note ? note.title : "[missing]"; + useLegacyImperativeHandlers(useMemo(() => ({ + loadReferenceLinkTitle: async ($el: JQuery, href: string) => { + const { noteId } = link.parseNavigationStateFromUrl(href); + const note = noteId ? await froca.getNote(noteId, true) : null; + const title = note ? note.title : "[missing]"; - $el.text(title); - }, - createNoteForReferenceLink: async (title: string) => { - let result; - if (notePath) { - result = await note_create.createNoteWithTypePrompt( - notePath, - { - activate: false, - title, - }, - ); - } + $el.text(title); + }, + createNoteForReferenceLink: async (title: string) => { + let result; + if (notePath) { + result = await note_create.createNoteWithTypePrompt(notePath, { + activate: false, + title + }); + } - return result?.note?.getBestNotePathString(); - }, - }), - [notePath], - ), - ); + return result?.note?.getBestNotePathString(); + } + }), [ notePath ])); // Keyboard shortcuts useTriliumEvent("addNewLabel", ({ ntxId: eventNtxId }) => { @@ -344,217 +276,135 @@ export default function AttributeEditor({ }); // Imperative API - useImperativeHandle( - api, - () => ({ - save, - refresh, - renderOwnedAttributes: (attributes) => - renderOwnedAttributes(attributes as FAttribute[], false), - focus: () => editorRef.current?.focus(), - }), - [save, refresh, renderOwnedAttributes], - ); + useImperativeHandle(api, () => ({ + save, + refresh, + renderOwnedAttributes: (attributes) => renderOwnedAttributes(attributes as FAttribute[], false), + focus: () => editorRef.current?.focus() + }), [ save, refresh, renderOwnedAttributes ]); return ( <> - {!hidden && ( -
{ - // Skip processing during IME composition - if (isIMEComposing(e)) { - return; - } + {!hidden &&
{ + // Skip processing during IME composition + if (isIMEComposing(e)) { + return; + } - if (e.key === "Enter") { - // allow autocomplete to fill the result textarea - setTimeout(() => save(), 100); - } - }} - > - {" "} -
- { - currentValueRef.current = currentValue ?? ""; + if (e.key === "Enter") { + // allow autocomplete to fill the result textarea + setTimeout(() => save(), 100); + } + }} + >
+ { + currentValueRef.current = currentValue ?? ""; - const oldValue = getPreprocessedData( - lastSavedContent.current ?? "", - ).trimEnd(); - const newValue = getPreprocessedData( - currentValue ?? "", - ).trimEnd(); - setNeedsSaving(oldValue !== newValue); - setError(undefined); - }} - onClick={(e, pos) => { - if (pos && pos.textNode && pos.textNode.data) { - const clickIndex = getClickIndex(pos); + const oldValue = getPreprocessedData(lastSavedContent.current ?? "").trimEnd(); + const newValue = getPreprocessedData(currentValue ?? "").trimEnd(); + setNeedsSaving(oldValue !== newValue); + setError(undefined); + }} + // onClick={(e, pos) => { + // if (pos && pos.textNode && pos.textNode.data) { + // const clickIndex = getClickIndex(pos); - let parsedAttrs: Attribute[]; + // let parsedAttrs: Attribute[]; - try { - parsedAttrs = - attribute_parser.lexAndParse( - getPreprocessedData( - currentValueRef.current, - ), - true, - ); - } catch (e: unknown) { - // the input is incorrect because the user messed up with it and now needs to fix it manually - console.log(e); - return null; - } - - let matchedAttr: Attribute | null = null; - - for (const attr of parsedAttrs) { - if ( - attr.startIndex !== undefined && - clickIndex > attr.startIndex && - attr.endIndex !== undefined && - clickIndex <= attr.endIndex - ) { - matchedAttr = attr; - break; - } - } - - setTimeout(() => { - if (matchedAttr) { - attributeDetailWidget.showAttributeDetail( - { - allAttributes: parsedAttrs, - attribute: matchedAttr, - isOwned: true, - x: e.pageX, - y: e.pageY, - }, - ); - setState("showAttributeDetail"); - } else { - setState("showHelpTooltip"); - } - }, 100); - } else { - setState("showHelpTooltip"); - } - }} - onKeyDown={() => attributeDetailWidget.hide()} - onBlur={() => save()} - onInitialized={() => editorRef.current?.focus()} - disableNewlines - disableSpellcheck - /> - -
- {needsSaving && ( - - )} - - { - // Prevent automatic hiding of the context menu due to the button being clicked. - e.stopPropagation(); - if (isMenuOpen) { - // If we re-show the menu, ContextMenu.show() will call hide() - // and immediately trigger onHide. Suppress that transient hide. - suppressNextOnHide.current = true; - } - setIsMenuOpen(true); + // try { + // parsedAttrs = attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current), true); + // } catch (e: unknown) { + // // the input is incorrect because the user messed up with it and now needs to fix it manually + // console.log(e); + // return null; + // } + onClick={(e) => { + // Prevent automatic hiding of the context menu due to the button being clicked. + e.stopPropagation(); + if (isMenuOpen) { + // If we re-show the menu, ContextMenu.show() will call hide() + // and immediately trigger onHide. Suppress that transient hide. + suppressNextOnHide.current = true; + } + setIsMenuOpen(true); - contextMenu.show({ - x: e.pageX, - y: e.pageY, - orientation: "left", + contextMenu.show({ + x: e.pageX, + y: e.pageY, + orientation: "left", - items: [ - { - title: t( - "attribute_editor.add_new_label", - ), - command: "addNewLabel", - uiIcon: "bx bx-hash", - }, - { - title: t( - "attribute_editor.add_new_relation", - ), - command: "addNewRelation", - uiIcon: "bx bx-transfer", - }, - { kind: "separator" }, - { - title: t( - "attribute_editor.add_new_label_definition", - ), - command: + items: [ + { + title: t( + "attribute_editor.add_new_label", + ), + command: "addNewLabel", + uiIcon: "bx bx-hash", + }, + { + title: t( + "attribute_editor.add_new_relation", + ), + command: "addNewRelation", + uiIcon: "bx bx-transfer", + }, + { kind: "separator" }, + { + title: t( + "attribute_editor.add_new_label_definition", + ), + command: "addNewLabelDefinition", - uiIcon: "bx bx-empty", - }, - { - title: t( - "attribute_editor.add_new_relation_definition", - ), - command: + uiIcon: "bx bx-empty", + }, + { + title: t( + "attribute_editor.add_new_relation_definition", + ), + command: "addNewRelationDefinition", - uiIcon: "bx bx-empty", - }, - ], - selectMenuItemHandler: (item) => - handleAddNewAttributeCommand( - item.command, - ), - onHide: () => { - if (suppressNextOnHide.current) { - suppressNextOnHide.current = false; - return; - } - setIsMenuOpen(false); - }, - }); - }} - /> -
-
- {error && ( -
- {getErrorMessage(error)} -
- )} + uiIcon: "bx bx-empty", + }, + ], + selectMenuItemHandler: (item) => + handleAddNewAttributeCommand( + item.command, + ), + onHide: () => { + if (suppressNextOnHide.current) { + suppressNextOnHide.current = false; + return; + } + setIsMenuOpen(false); + }, + }); + }} + />
- )} + + { error && ( +
+ {getErrorMessage(error)} +
+ )} +
} {createPortal(attributeDetailWidgetEl, document.body)} @@ -585,4 +435,4 @@ function getClickIndex(pos: ModelPosition) { } return clickIndex; -} +} \ No newline at end of file From f64b0290092edcc65af9edec06a35392f0246d04 Mon Sep 17 00:00:00 2001 From: Zaki Ur Rehman Date: Thu, 16 Apr 2026 23:04:29 +0500 Subject: [PATCH 4/4] refactor: ensure only the changed lines are shown --- .../ribbon/components/AttributeEditor.tsx | 158 ++++++++++-------- 1 file changed, 88 insertions(+), 70 deletions(-) diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx index dd5eb8895b..1e5bb77500 100644 --- a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -322,81 +322,99 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI setNeedsSaving(oldValue !== newValue); setError(undefined); }} - // onClick={(e, pos) => { - // if (pos && pos.textNode && pos.textNode.data) { - // const clickIndex = getClickIndex(pos); + onClick={(e, pos) => { + if (pos && pos.textNode && pos.textNode.data) { + const clickIndex = getClickIndex(pos); - // let parsedAttrs: Attribute[]; + let parsedAttrs: Attribute[]; - // try { - // parsedAttrs = attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current), true); - // } catch (e: unknown) { - // // the input is incorrect because the user messed up with it and now needs to fix it manually - // console.log(e); - // return null; - // } - onClick={(e) => { - // Prevent automatic hiding of the context menu due to the button being clicked. - e.stopPropagation(); - if (isMenuOpen) { + try { + parsedAttrs = attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current), true); + } catch (e: unknown) { + // the input is incorrect because the user messed up with it and now needs to fix it manually + console.log(e); + return null; + } + + let matchedAttr: Attribute | null = null; + + for (const attr of parsedAttrs) { + if (attr.startIndex !== undefined && clickIndex > attr.startIndex && + attr.endIndex !== undefined && clickIndex <= attr.endIndex) { + matchedAttr = attr; + break; + } + } + + setTimeout(() => { + if (matchedAttr) { + attributeDetailWidget.showAttributeDetail({ + allAttributes: parsedAttrs, + attribute: matchedAttr, + isOwned: true, + x: e.pageX, + y: e.pageY + }); + setState("showAttributeDetail"); + } else { + setState("showHelpTooltip"); + } + }, 100); + } else { + setState("showHelpTooltip"); + } + }} + onKeyDown={() => attributeDetailWidget.hide()} + onBlur={() => save()} + onInitialized={() => editorRef.current?.focus()} + disableNewlines disableSpellcheck + /> + +
+ { needsSaving && } + + { + // Prevent automatic hiding of the context menu due to the button being clicked. + e.stopPropagation(); + if (isMenuOpen) { // If we re-show the menu, ContextMenu.show() will call hide() // and immediately trigger onHide. Suppress that transient hide. - suppressNextOnHide.current = true; - } - setIsMenuOpen(true); - - contextMenu.show({ - x: e.pageX, - y: e.pageY, - orientation: "left", - - items: [ - { - title: t( - "attribute_editor.add_new_label", - ), - command: "addNewLabel", - uiIcon: "bx bx-hash", + suppressNextOnHide.current = true; + } + setIsMenuOpen(true); + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + orientation: "left", + items: [ + { title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" }, + { title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" }, + { kind: "separator" }, + { title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" }, + { title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" } + ], + selectMenuItemHandler: (item) => handleAddNewAttributeCommand(item.command), + onHide: () => { + if (suppressNextOnHide.current) { + suppressNextOnHide.current = false; + return; + } + setIsMenuOpen(false); }, - { - title: t( - "attribute_editor.add_new_relation", - ), - command: "addNewRelation", - uiIcon: "bx bx-transfer", - }, - { kind: "separator" }, - { - title: t( - "attribute_editor.add_new_label_definition", - ), - command: - "addNewLabelDefinition", - uiIcon: "bx bx-empty", - }, - { - title: t( - "attribute_editor.add_new_relation_definition", - ), - command: - "addNewRelationDefinition", - uiIcon: "bx bx-empty", - }, - ], - selectMenuItemHandler: (item) => - handleAddNewAttributeCommand( - item.command, - ), - onHide: () => { - if (suppressNextOnHide.current) { - suppressNextOnHide.current = false; - return; - } - setIsMenuOpen(false); - }, - }); - }} - /> + }); + }} + /> +
{ error && (