refactor: show only modified lines

This commit is contained in:
Zaki Ur Rehman
2026-04-16 22:54:48 +05:00
parent 6bc3176251
commit b2f02962fc

View File

@@ -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<CommandData>;
@@ -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<string[]>(
`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`,
);
const names = await server.get<string[]>(`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<string[]>(
`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`,
);
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `~${name}`,
name,
name
};
});
},
minimumCharacters: 0,
},
minimumCharacters: 0
}
];
interface AttributeEditorProps {
api: MutableRef<AttributeEditorImperativeHandlers | null>;
note: FNote;
@@ -123,39 +93,29 @@ export interface AttributeEditorImperativeHandlers {
renderOwnedAttributes(ownedAttributes: FAttribute[]): Promise<void>;
}
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<unknown>();
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<unknown>();
const [ needsSaving, setNeedsSaving ] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const suppressNextOnHide = useRef(false);
const lastSavedContent = useRef<string>();
const currentValueRef = useRef(currentValue);
const wrapperRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<CKEditorApi>();
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 = `<p>${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}</p>`;
let htmlAttrs = (`<p>${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}</p>`);
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<HTMLElement>,
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<HTMLElement>, 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 && (
<div
className="attribute-list-editor-wrapper"
ref={wrapperRef}
style="position: relative; padding-top: 10px; padding-bottom: 10px"
onKeyDown={(e) => {
// Skip processing during IME composition
if (isIMEComposing(e)) {
return;
}
{!hidden && <div
className="attribute-list-editor-wrapper"
ref={wrapperRef}
style="position: relative; padding-top: 10px; padding-bottom: 10px"
onKeyDown={(e) => {
// Skip processing during IME composition
if (isIMEComposing(e)) {
return;
}
if (e.key === "Enter") {
// allow autocomplete to fill the result textarea
setTimeout(() => save(), 100);
}
}}
>
{" "}
<div style="position: relative;">
<CKEditor
apiRef={editorRef}
className="attribute-list-editor"
tabIndex={200}
editor={CKEditorAttributeEditor}
currentValue={currentValue}
config={{
toolbar: { items: [] },
placeholder: t("attribute_editor.placeholder"),
mention: { feeds: mentionSetup },
licenseKey: "GPL",
language: "en",
}}
onChange={(currentValue) => {
currentValueRef.current = currentValue ?? "";
if (e.key === "Enter") {
// allow autocomplete to fill the result textarea
setTimeout(() => save(), 100);
}
}}
> <div style="position: relative;">
<CKEditor
apiRef={editorRef}
className="attribute-list-editor"
tabIndex={200}
editor={CKEditorAttributeEditor}
currentValue={currentValue}
config={{
toolbar: { items: [] },
placeholder: t("attribute_editor.placeholder"),
mention: { feeds: mentionSetup },
licenseKey: "GPL",
language: "en"
}}
onChange={(currentValue) => {
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
/>
<div className="attribute-editor-buttons">
{needsSaving && (
<ActionButton
icon="bx bx-save"
className="save-attributes-button tn-tool-button"
text={escapeQuotes(
t("attribute_editor.save_attributes"),
)}
onClick={save}
/>
)}
<ActionButton
icon="bx bx-plus"
className="add-new-attribute-button tn-tool-button"
text={
isMenuOpen
? ""
: escapeQuotes(t("attribute_editor.add_a_new_attribute"))
}
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);
// 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<AttributeCommandNames>({
x: e.pageX,
y: e.pageY,
orientation: "left",
contextMenu.show<AttributeCommandNames>({
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);
},
});
}}
/>
</div>
</div>
{error && (
<div className="attribute-errors">
{getErrorMessage(error)}
</div>
)}
uiIcon: "bx bx-empty",
},
],
selectMenuItemHandler: (item) =>
handleAddNewAttributeCommand(
item.command,
),
onHide: () => {
if (suppressNextOnHide.current) {
suppressNextOnHide.current = false;
return;
}
setIsMenuOpen(false);
},
});
}}
/>
</div>
)}
{ error && (
<div className="attribute-errors">
{getErrorMessage(error)}
</div>
)}
</div>}
{createPortal(attributeDetailWidgetEl, document.body)}
</>
@@ -585,4 +435,4 @@ function getClickIndex(pos: ModelPosition) {
}
return clickIndex;
}
}