mirror of
https://github.com/zadam/trilium.git
synced 2026-09-12 19:50:20 +05:00
fix(text): cut selection into sub-note loses the selection (closes #9890)
The command took the note and parent path from the tab manager, and createNote() dropped saveSelection whenever the *active* note was not a text note. But the editor firing the command is not always the active tab's: it also runs in a split, the quick editor, the tree popup and the embedded panes of the map and calendar views, whose note contexts live outside the tab manager. There the selection was discarded without a word — an empty sub-note under an unrelated parent, the source text left where it was. Which is the "randomly" of the report: it turns on what the active tab happens to hold, not on what was selected. Both are now answered by the editor that fired the command: its own note context for the note and parent, and its own editor rather than noteContext.getTextEditor(), which races the round trip through the event bus against a 200 ms timeout and resolves to null when it loses — the same empty sub-note by another road. Cutting with nothing selected says so instead of creating an empty note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
563d355f6b
commit
a072f1a4d3
@ -205,22 +205,57 @@ describe("createNote", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("disables saveSelection when the active context note type is not text", async () => {
|
||||
it("saves the selection of the editor it was handed, whatever the active context shows", async () => {
|
||||
// The editor handing us the selection belongs to a split / the quick editor / an embedded
|
||||
// pane, so the active context is a different note of a different type entirely (#9890).
|
||||
setActiveContext(true);
|
||||
tabManager.activeNoteType = "code";
|
||||
tabManager.activeNoteType = "book";
|
||||
const removeSelection = vi.fn();
|
||||
const textEditor = {
|
||||
getSelectedHtml: vi.fn(() => "<h1>x</h1>"),
|
||||
getSelectedHtml: vi.fn(() => "<h1>Heading</h1><p>body</p>"),
|
||||
removeSelection
|
||||
} as any;
|
||||
|
||||
await noteCreateService.createNote("root", { saveSelection: true, textEditor });
|
||||
|
||||
// selection parsing was skipped, so getSelectedHtml/removeSelection untouched
|
||||
expect(textEditor.getSelectedHtml).not.toHaveBeenCalled();
|
||||
expect(server.post).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ title: "Heading", content: "<p>body</p>" }),
|
||||
undefined
|
||||
);
|
||||
expect(removeSelection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates a plain empty note without touching the source when there is nothing selected", async () => {
|
||||
setActiveContext(true);
|
||||
const removeSelection = vi.fn();
|
||||
const textEditor = {
|
||||
getSelectedHtml: vi.fn(() => ""),
|
||||
removeSelection
|
||||
} as any;
|
||||
|
||||
await noteCreateService.createNote("root", { saveSelection: true, textEditor });
|
||||
|
||||
expect(server.post).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ title: undefined, content: "" }),
|
||||
undefined
|
||||
);
|
||||
expect(removeSelection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not attempt to save a selection when no text editor was passed", async () => {
|
||||
setActiveContext(true);
|
||||
|
||||
await noteCreateService.createNote("root", { saveSelection: true, title: "Plain" });
|
||||
|
||||
expect(server.post).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ title: "Plain", content: "" }),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it("honors an explicit target and targetBranchId in the URL", async () => {
|
||||
setActiveContext(true);
|
||||
await noteCreateService.createNote("root", { target: "after", targetBranchId: "tb-9" });
|
||||
|
||||
@ -65,14 +65,17 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
||||
options.isProtected = false;
|
||||
}
|
||||
|
||||
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
|
||||
// Whether there is a selection to save is the editor's answer, not the tab manager's: the editor
|
||||
// handing us the selection is not necessarily the active tab's (a split, the quick editor, an
|
||||
// embedded pane). An empty selection means there is nothing to cut, so the note is created as an
|
||||
// ordinary empty child and the source note is left untouched.
|
||||
const selectedHtml = options.saveSelection ? options.textEditor?.getSelectedHtml() : null;
|
||||
if (selectedHtml) {
|
||||
[options.title, options.content] = parseSelectedHtml(selectedHtml);
|
||||
} else {
|
||||
options.saveSelection = false;
|
||||
}
|
||||
|
||||
if (options.saveSelection && options.textEditor) {
|
||||
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
|
||||
}
|
||||
|
||||
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
|
||||
|
||||
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
|
||||
|
||||
@ -1557,7 +1557,8 @@
|
||||
"editor_crashed_details_intro": "If you experience this error several times, consider reporting it on GitHub by pasting the information below.",
|
||||
"editor_crashed_details_title": "Technical information",
|
||||
"auto-detect-language": "Auto-detected",
|
||||
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report."
|
||||
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report.",
|
||||
"nothing_selected_to_cut": "Select some text first to cut it into a sub-note."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Open a note by typing the note's title into the input below or choose a note in the tree.",
|
||||
|
||||
@ -196,18 +196,30 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
}
|
||||
},
|
||||
async cutIntoNoteCommand() {
|
||||
const note = appContext.tabManager.getActiveContextNote();
|
||||
if (!note) return;
|
||||
// The note this editor is showing, not whichever one the tab manager considers active: the
|
||||
// editor also runs in a split, in the quick editor and in the embedded panes of the map and
|
||||
// calendar views, where the active context is a different note entirely. Asking the tab
|
||||
// manager there put the sub-note under an unrelated parent, and — since the selection was
|
||||
// only saved when the *active* note was a text note — usually created it empty (#9890).
|
||||
const sourceNote = noteContext?.note;
|
||||
const parentNotePath = noteContext?.notePath;
|
||||
// This component's own editor, rather than noteContext.getTextEditor(): that one races the
|
||||
// round trip through the event bus against a 200 ms timeout and resolves to null when it
|
||||
// loses, which again meant an empty sub-note and a selection left where it was.
|
||||
const textEditor = await waitForEditor() as CKTextEditor | undefined;
|
||||
if (!sourceNote || !parentNotePath || !textEditor) return;
|
||||
|
||||
if (!textEditor.getSelectedHtml()) {
|
||||
toast.showMessage(t("editable_text.nothing_selected_to_cut"));
|
||||
return;
|
||||
}
|
||||
|
||||
// without await as this otherwise causes deadlock through component mutex
|
||||
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (noteContext && parentNotePath) {
|
||||
note_create.createNote(parentNotePath, {
|
||||
isProtected: note.isProtected,
|
||||
isProtected: sourceNote.isProtected,
|
||||
saveSelection: true,
|
||||
textEditor: await noteContext?.getTextEditor()
|
||||
textEditor
|
||||
});
|
||||
}
|
||||
},
|
||||
async saveNoteDetailNowCommand() {
|
||||
// used by cutToNote in CKEditor build
|
||||
|
||||
@ -67,6 +67,13 @@ describe("CutToNotePlugin", () => {
|
||||
expect(html).not.toContain("data-list-item-id");
|
||||
});
|
||||
|
||||
it("returns an empty string from getSelectedHtml when nothing is selected", () => {
|
||||
// What the host takes as "there is nothing to cut here" before it creates a sub-note (#9890).
|
||||
setModelData(editor.model, "<paragraph>foo[]bar</paragraph>");
|
||||
|
||||
expect(editor.getSelectedHtml()).toBe("");
|
||||
});
|
||||
|
||||
it("removeSelection deletes the selection, inserts a paragraph and saves the note", async () => {
|
||||
setModelData(editor.model, "<paragraph>foo[bar]baz</paragraph>");
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user