From f1e8c25d6153e606e73d5fd0190fcd04a9031703 Mon Sep 17 00:00:00 2001 From: Jakob Schlanstedt Date: Sun, 12 Oct 2025 06:31:48 +0200 Subject: [PATCH 01/59] feat(search): add create into inbox to search --- apps/client/src/components/entrypoints.ts | 19 +- .../src/components/main_tree_executors.ts | 4 +- .../src/components/root_command_executor.ts | 2 +- apps/client/src/menus/tree_context_menu.ts | 4 +- apps/client/src/services/note_autocomplete.ts | 480 ++++++++++++------ apps/client/src/services/note_create.ts | 53 +- .../src/translations/en/translation.json | 5 +- .../attribute_widgets/attribute_detail.ts | 4 +- .../src/widgets/collections/board/api.ts | 4 +- .../src/widgets/collections/table/columns.tsx | 3 +- .../widgets/collections/table/row_editing.ts | 2 +- apps/client/src/widgets/dialogs/add_link.tsx | 4 +- .../src/widgets/dialogs/include_note.tsx | 4 +- .../src/widgets/dialogs/jump_to_note.tsx | 75 ++- .../src/widgets/dialogs/note_type_chooser.tsx | 4 +- .../src/widgets/llm_chat/llm_chat_panel.ts | 4 +- .../mobile_widgets/mobile_detail_menu.tsx | 2 +- apps/client/src/widgets/note_tree.ts | 4 +- .../client/src/widgets/promoted_attributes.ts | 4 +- .../ribbon/components/AttributeEditor.tsx | 44 +- .../src/widgets/type_widgets/text/config.ts | 4 +- packages/ckeditor5/src/augmentation.ts | 11 +- .../src/plugins/mention_customization.ts | 55 +- 23 files changed, 530 insertions(+), 265 deletions(-) diff --git a/apps/client/src/components/entrypoints.ts b/apps/client/src/components/entrypoints.ts index 8a902666f9..a28ac6fab1 100644 --- a/apps/client/src/components/entrypoints.ts +++ b/apps/client/src/components/entrypoints.ts @@ -11,6 +11,7 @@ import froca from "../services/froca.js"; import linkService from "../services/link.js"; import { t } from "../services/i18n.js"; import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; +import noteCreateService from "../services/note_create"; export default class Entrypoints extends Component { constructor() { @@ -24,23 +25,7 @@ export default class Entrypoints extends Component { } async createNoteIntoInboxCommand() { - const inboxNote = await dateNoteService.getInboxNote(); - if (!inboxNote) { - console.warn("Missing inbox note."); - return; - } - - const { note } = await server.post(`notes/${inboxNote.noteId}/children?target=into`, { - content: "", - type: "text", - isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable() - }); - - await ws.waitForMaxKnownEntityChangeId(); - - await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, { activate: true }); - - appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true }); + await noteCreateService.createNoteIntoInbox(); } async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) { diff --git a/apps/client/src/components/main_tree_executors.ts b/apps/client/src/components/main_tree_executors.ts index 9bc037ccf3..e29251b5c9 100644 --- a/apps/client/src/components/main_tree_executors.ts +++ b/apps/client/src/components/main_tree_executors.ts @@ -48,7 +48,7 @@ export default class MainTreeExecutors extends Component { return; } - await noteCreateService.createNote(activeNoteContext.notePath, { + await noteCreateService.createNoteIntoPath(activeNoteContext.notePath, { isProtected: activeNoteContext.note.isProtected, saveSelection: false }); @@ -72,7 +72,7 @@ export default class MainTreeExecutors extends Component { return; } - await noteCreateService.createNote(parentNotePath, { + await noteCreateService.createNoteIntoPath(parentNotePath, { target: "after", targetBranchId: node.data.branchId, isProtected: isProtected, diff --git a/apps/client/src/components/root_command_executor.ts b/apps/client/src/components/root_command_executor.ts index 4a1c987f76..b953613ab7 100644 --- a/apps/client/src/components/root_command_executor.ts +++ b/apps/client/src/components/root_command_executor.ts @@ -240,7 +240,7 @@ export default class RootCommandExecutor extends Component { // Create a new AI Chat note at the root level const rootNoteId = "root"; - const result = await noteCreateService.createNote(rootNoteId, { + const result = await noteCreateService.createNoteIntoPath(rootNoteId, { title: "New AI Chat", type: "aiChat", content: JSON.stringify({ diff --git a/apps/client/src/menus/tree_context_menu.ts b/apps/client/src/menus/tree_context_menu.ts index 19e6f4e169..cbd6835b9c 100644 --- a/apps/client/src/menus/tree_context_menu.ts +++ b/apps/client/src/menus/tree_context_menu.ts @@ -287,7 +287,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener((res, rej) => { +async function autocompleteSourceForCKEditor( + queryText: string, + createMode: CreateMode +): Promise { + // Wrap the callback-based autocompleteSource in a Promise for async/await + const rows = await new Promise((resolve) => { autocompleteSource( queryText, - (rows) => { - res( - rows.map((row) => { - return { - action: row.action, - noteTitle: row.noteTitle, - id: `@${row.notePathTitle}`, - name: row.notePathTitle || "", - link: `#${row.notePath}`, - notePath: row.notePath, - highlightedNotePathTitle: row.highlightedNotePathTitle - }; - }) - ); - }, + (suggestions) => resolve(suggestions), { - allowCreatingNotes: true + createMode, } ); }); + + // Map internal suggestions to CKEditor mention feed items + return rows.map((row): ExtendedMentionFeedObjectItem => ({ + action: row.action?.toString(), + noteTitle: row.noteTitle, + id: `@${row.notePathTitle}`, + name: row.notePathTitle || "", + link: `#${row.notePath}`, + notePath: row.notePath, + parentNoteId: row.parentNoteId, + highlightedNotePathTitle: row.highlightedNotePathTitle + })); } -async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) { +async function autocompleteSource( + term: string, + callback: (rows: Suggestion[]) => void, + options: Options = {} +) { // Check if we're in command mode if (options.isCommandPalette && term.startsWith(">")) { const commandQuery = term.substring(1).trim(); // Get commands (all if no query, filtered if query provided) - const commands = commandQuery.length === 0 - ? commandRegistry.getAllCommands() - : commandRegistry.searchCommands(commandQuery); + const commands = + commandQuery.length === 0 + ? commandRegistry.getAllCommands() + : commandRegistry.searchCommands(commandQuery); // Convert commands to suggestions - const commandSuggestions: Suggestion[] = commands.map(cmd => ({ - action: "command", + const commandSuggestions: Suggestion[] = commands.map((cmd) => ({ + action: SuggestionAction.Command, commandId: cmd.id, noteTitle: cmd.name, notePathTitle: `>${cmd.name}`, highlightedNotePathTitle: cmd.name, commandDescription: cmd.description, commandShortcut: cmd.shortcut, - icon: cmd.icon + icon: cmd.icon, })); - cb(commandSuggestions); + callback(commandSuggestions); return; } - const fastSearch = options.fastSearch === false ? false : true; - if (fastSearch === false) { - if (term.trim().length === 0) { - return; - } - cb([ + const fastSearch = options.fastSearch !== false; + const trimmedTerm = term.trim(); + const activeNoteId = appContext.tabManager.getActiveContextNoteId(); + + if (!fastSearch && trimmedTerm.length === 0) return; + + if (!fastSearch) { + callback([ { - noteTitle: term, - highlightedNotePathTitle: t("quick-search.searching") - } + noteTitle: trimmedTerm, + highlightedNotePathTitle: t("quick-search.searching"), + }, ]); } - const activeNoteId = appContext.tabManager.getActiveContextNoteId(); - const length = term.trim().length; - - let results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`); + let results = await server.get( + `autocomplete?query=${encodeURIComponent(trimmedTerm)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}` + ); options.fastSearch = true; - if (length >= 1 && options.allowCreatingNotes) { - results = [ - { - action: "create-note", - noteTitle: term, - parentNoteId: activeNoteId || "root", - highlightedNotePathTitle: t("note_autocomplete.create-note", { term }) - } as Suggestion - ].concat(results); + // --- Create Note suggestions --- + if (trimmedTerm.length >= 1) { + switch (options.createMode) { + case CreateMode.CreateOnly: { + results = [ + { + action: SuggestionAction.CreateNoteIntoInbox, + noteTitle: trimmedTerm, + parentNoteId: "inbox", + highlightedNotePathTitle: t("note_autocomplete.create-note-into-inbox", { term: trimmedTerm }), + }, + { + action: SuggestionAction.CreateNoteIntoPath, + noteTitle: trimmedTerm, + parentNoteId: activeNoteId || "root", + highlightedNotePathTitle: t("note_autocomplete.create-note-into-path", { term: trimmedTerm }), + }, + ...results, + ]; + break; + } + + case CreateMode.CreateAndLink: { + results = [ + { + action: SuggestionAction.CreateAndLinkNoteIntoInbox, + noteTitle: trimmedTerm, + parentNoteId: "inbox", + highlightedNotePathTitle: t("note_autocomplete.create-and-link-note-into-inbox", { term: trimmedTerm }), + }, + { + action: SuggestionAction.CreateAndLinkNoteIntoPath, + noteTitle: trimmedTerm, + parentNoteId: activeNoteId || "root", + highlightedNotePathTitle: t("note_autocomplete.create-and-link-note-into-path", { term: trimmedTerm }), + }, + ...results, + ]; + break; + } + + default: + // CreateMode.None or undefined → no creation suggestions + break; + } } - if (length >= 1 && options.allowJumpToSearchNotes) { - results = results.concat([ + // --- Jump to Search Notes --- + if (trimmedTerm.length >= 1 && options.allowJumpToSearchNotes) { + results = [ + ...results, { - action: "search-notes", - noteTitle: term, - highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} Ctrl+Enter` - } - ]); + action: SuggestionAction.SearchNotes, + noteTitle: trimmedTerm, + highlightedNotePathTitle: `${t("note_autocomplete.search-for", { + term: trimmedTerm, + })} Ctrl+Enter`, + }, + ]; } - if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) { + // --- External Link suggestion --- + if (/^[a-z]+:\/\/.+/i.test(trimmedTerm) && options.allowExternalLinks) { results = [ { - action: "external-link", - externalLink: term, - highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term }) - } as Suggestion - ].concat(results); + action: SuggestionAction.ExternalLink, + externalLink: trimmedTerm, + highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term: trimmedTerm }), + }, + ...results, + ]; } - cb(results); + callback(results); } function clearText($el: JQuery) { @@ -198,6 +290,64 @@ function fullTextSearch($el: JQuery, options: Options) { $el.autocomplete("val", searchString); } +function renderCommandSuggestion(s: Suggestion): string { + const icon = s.icon || "bx bx-terminal"; + const shortcut = s.commandShortcut + ? `${s.commandShortcut}` + : ""; + + return ` +
+ +
+
${s.highlightedNotePathTitle}
+ ${s.commandDescription ? `
${s.commandDescription}
` : ""} +
+ ${shortcut} +
+ `; +} + +function renderNoteSuggestion(s: Suggestion): string { + const actionClass = + s.action === SuggestionAction.SearchNotes ? "search-notes-action" : ""; + + const iconClass = (() => { + switch (s.action) { + case SuggestionAction.SearchNotes: + return "bx bx-search"; + case SuggestionAction.CreateAndLinkNoteIntoInbox: + case SuggestionAction.CreateNoteIntoInbox: + return "bx bx-plus"; + case SuggestionAction.CreateAndLinkNoteIntoPath: + case SuggestionAction.CreateNoteIntoPath: + return "bx bx-plus"; + case SuggestionAction.ExternalLink: + return "bx bx-link-external"; + default: + return s.icon ?? "bx bx-note"; + } + })(); + + return ` +
+ + + ${s.highlightedNotePathTitle} + ${s.highlightedAttributeSnippet + ? `${s.highlightedAttributeSnippet}` + : ""} + +
+ `; +} + +function renderSuggestion(suggestion: Suggestion): string { + return suggestion.action === SuggestionAction.Command + ? renderCommandSuggestion(suggestion) + : renderNoteSuggestion(suggestion); +} + function initNoteAutocomplete($el: JQuery, options?: Options) { if ($el.hasClass("note-autocomplete-input")) { // clear any event listener added in previous invocation of this function @@ -283,24 +433,21 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { $el.autocomplete( { ...autocompleteOptions, - appendTo: document.querySelector("body"), + appendTo: document.body, hint: false, autoselect: true, - // openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces - // re-querying of the autocomplete source which then changes the currently selected suggestion openOnFocus: false, minLength: 0, - tabAutocomplete: false + tabAutocomplete: false, }, [ { - source: (term, cb) => { + source: (term, callback) => { clearTimeout(debounceTimeoutId); debounceTimeoutId = setTimeout(() => { - if (isComposingInput) { - return; + if (!isComposingInput) { + autocompleteSource(term, callback, options); } - autocompleteSource(term, cb, options); }, searchDelay); if (searchDelay === 0) { @@ -308,109 +455,124 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { } }, displayKey: "notePathTitle", - templates: { - suggestion: (suggestion) => { - if (suggestion.action === "command") { - let html = `
`; - html += ``; - html += `
`; - html += `
${suggestion.highlightedNotePathTitle}
`; - if (suggestion.commandDescription) { - html += `
${suggestion.commandDescription}
`; - } - html += `
`; - if (suggestion.commandShortcut) { - html += `${suggestion.commandShortcut}`; - } - html += '
'; - return html; - } - // Add special class for search-notes action - const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : ""; - - // Choose appropriate icon based on action - let iconClass = suggestion.icon ?? "bx bx-note"; - if (suggestion.action === "search-notes") { - iconClass = "bx bx-search"; - } else if (suggestion.action === "create-note") { - iconClass = "bx bx-plus"; - } else if (suggestion.action === "external-link") { - iconClass = "bx bx-link-external"; - } - - // Simplified HTML structure without nested divs - let html = `
`; - html += ``; - html += ``; - html += `${suggestion.highlightedNotePathTitle}`; - - // Add attribute snippet inline if available - if (suggestion.highlightedAttributeSnippet) { - html += `${suggestion.highlightedAttributeSnippet}`; - } - - html += ``; - html += `
`; - return html; - } - }, - // we can't cache identical searches because notes can be created / renamed, new recent notes can be added - cache: false - } + templates: { suggestion: renderSuggestion }, + cache: false, + }, ] ); // TODO: Types fail due to "autocomplete:selected" not being registered in type definitions. ($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => { - if (suggestion.action === "command") { - $el.autocomplete("close"); - $el.trigger("autocomplete:commandselected", [suggestion]); - return; - } + switch (suggestion.action) { + case SuggestionAction.Command: { + $el.autocomplete("close"); + $el.trigger("autocomplete:commandselected", [suggestion]); + break; + } - if (suggestion.action === "external-link") { - $el.setSelectedNotePath(null); - $el.setSelectedExternalLink(suggestion.externalLink); + case SuggestionAction.ExternalLink: { + $el.setSelectedNotePath(null); + $el.setSelectedExternalLink(suggestion.externalLink); + $el.autocomplete("val", suggestion.externalLink); + $el.autocomplete("close"); + $el.trigger("autocomplete:externallinkselected", [suggestion]); + break; + } - $el.autocomplete("val", suggestion.externalLink); + // --- CREATE NOTE INTO INBOX --- + case SuggestionAction.CreateNoteIntoInbox: { + const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType(); + if (!success) return; - $el.autocomplete("close"); + const { note } = await noteCreateService.createNoteIntoInbox({ + title: suggestion.noteTitle, + activate: true, + type: noteType, + templateNoteId, + }); - $el.trigger("autocomplete:externallinkselected", [suggestion]); + const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; + suggestion.notePath = note?.getBestNotePathString(hoistedNoteId); - return; - } + $el.trigger("autocomplete:noteselected", [suggestion]); + $el.autocomplete("close"); + break; + } + + // --- CREATE AND LINK NOTE INTO INBOX --- + case SuggestionAction.CreateAndLinkNoteIntoInbox: { + const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType(); + if (!success) return; + + const { note } = await noteCreateService.createNoteIntoInbox({ + title: suggestion.noteTitle, + activate: false, + type: noteType, + templateNoteId, + }); - if (suggestion.action === "create-note") { - const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType(); - if (!success) { - return; + const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; + suggestion.notePath = note?.getBestNotePathString(hoistedNoteId); + + $el.trigger("autocomplete:noteselected", [suggestion]); + $el.autocomplete("close"); + break; } - const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, { - title: suggestion.noteTitle, - activate: false, - type: noteType, - templateNoteId: templateNoteId - }); - - const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; - suggestion.notePath = note?.getBestNotePathString(hoistedNoteId); - } - if (suggestion.action === "search-notes") { - const searchString = suggestion.noteTitle; - appContext.triggerCommand("searchNotes", { searchString }); - return; - } + // --- CREATE NOTE INTO PATH --- + case SuggestionAction.CreateNoteIntoPath: { + const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType(); + if (!success) return; + + const { note } = await noteCreateService.createNoteIntoPath(notePath || suggestion.parentNoteId, { + title: suggestion.noteTitle, + activate: true, + type: noteType, + templateNoteId, + }); - $el.setSelectedNotePath(suggestion.notePath); - $el.setSelectedExternalLink(null); + const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; + suggestion.notePath = note?.getBestNotePathString(hoistedNoteId); - $el.autocomplete("val", suggestion.noteTitle); + $el.trigger("autocomplete:noteselected", [suggestion]); + $el.autocomplete("close"); + break; + } + + // --- CREATE AND LINK NOTE INTO PATH --- + case SuggestionAction.CreateAndLinkNoteIntoPath: { + const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType(); + if (!success) return; + + const { note } = await noteCreateService.createNoteIntoPath(notePath || suggestion.parentNoteId, { + title: suggestion.noteTitle, + activate: false, + type: noteType, + templateNoteId, + }); - $el.autocomplete("close"); + const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; + suggestion.notePath = note?.getBestNotePathString(hoistedNoteId); + + $el.trigger("autocomplete:noteselected", [suggestion]); + $el.autocomplete("close"); + break; + } - $el.trigger("autocomplete:noteselected", [suggestion]); + case SuggestionAction.SearchNotes: { + const searchString = suggestion.noteTitle; + appContext.triggerCommand("searchNotes", { searchString }); + break; + } + + default: { + $el.setSelectedNotePath(suggestion.notePath); + $el.setSelectedExternalLink(null); + $el.autocomplete("val", suggestion.noteTitle); + $el.autocomplete("close"); + $el.trigger("autocomplete:noteselected", [suggestion]); + } + } }); $el.on("autocomplete:closed", () => { diff --git a/apps/client/src/services/note_create.ts b/apps/client/src/services/note_create.ts index 00ae717d21..9abd0573d0 100644 --- a/apps/client/src/services/note_create.ts +++ b/apps/client/src/services/note_create.ts @@ -10,6 +10,8 @@ import type FNote from "../entities/fnote.js"; import type FBranch from "../entities/fbranch.js"; import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; import type { CKTextEditor } from "@triliumnext/ckeditor5"; +import dateNoteService from "../services/date_notes.js"; +import { CreateChildrenResponse } from "@triliumnext/commons"; export interface CreateNoteOpts { isProtected?: boolean; @@ -37,7 +39,47 @@ interface DuplicateResponse { note: FNote; } -async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) { +/** + * Creates a new note inside the user's Inbox. + * + * @param {CreateNoteOpts} [options] - Optional settings such as title, type, template, or content. + * @returns {Promise<{ note: FNote | null; branch: FBranch | undefined }>} + * Resolves with the created note and its branch, or `{ note: null, branch: undefined }` if the inbox is missing. + */ +async function createNoteIntoInbox( + options: CreateNoteOpts = {} +): Promise<{ note: FNote | null; branch: FBranch | undefined }> { + const inboxNote = await dateNoteService.getInboxNote(); + if (!inboxNote) { + console.warn("Missing inbox note."); + // always return a defined object + return { note: null, branch: undefined }; + } + + if (options.isProtected === undefined) { + options.isProtected = + inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable(); + } + + const result = await createNoteIntoPath(inboxNote.noteId, { + ...options, + target: "into", + }); + + return result; +} +/** + * Core function that creates a new note under the specified parent note path. + * + * @param {string | undefined} parentNotePath - The parent note path where the new note will be created. + * @param {CreateNoteOpts} [options] - Options controlling note creation (title, content, type, template, focus, etc.). + * @returns {Promise<{ note: FNote | null; branch: FBranch | undefined }>} + * Resolves with the created note and branch entities. + */ +async function createNoteIntoPath( + parentNotePath: string | undefined, + options: CreateNoteOpts = {} +): Promise<{ note: FNote | null; branch: FBranch | undefined }> { options = Object.assign( { activate: true, @@ -113,7 +155,7 @@ async function chooseNoteType() { }); } -async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) { +async function createNoteIntoPathWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) { const { success, noteType, templateNoteId, notePath } = await chooseNoteType(); if (!success) { @@ -123,7 +165,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN options.type = noteType; options.templateNoteId = templateNoteId; - return await createNote(notePath || parentNotePath, options); + return await createNoteIntoPath(notePath || parentNotePath, options); } /* If the first element is heading, parse it out and use it as a new heading. */ @@ -158,8 +200,9 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) { } export default { - createNote, - createNoteWithTypePrompt, + createNoteIntoInbox, + createNoteIntoPath, + createNoteIntoPathWithTypePrompt, duplicateSubtree, chooseNoteType }; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index aa8c5926c9..336051632f 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1888,7 +1888,10 @@ }, "note_autocomplete": { "search-for": "Search for \"{{term}}\"", - "create-note": "Create and link child note \"{{term}}\"", + "create-note-into-path": "Create child note \"{{term}}\"", + "create-note-into-inbox": "Create in Inbox note \"{{term}}\"", + "create-and-link-note-into-path": "Create and link child note \"{{term}}\"", + "create-and-link-note-into-inbox": "Create in Inbox and link note \"{{term}}\"", "insert-external-link": "Insert external link to \"{{term}}\"", "clear-text-field": "Clear text field", "show-recent-notes": "Show recent notes", diff --git a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts index 2a7a55aef1..8f144f63d8 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts @@ -3,7 +3,7 @@ import server from "../../services/server.js"; import froca from "../../services/froca.js"; import linkService from "../../services/link.js"; import attributeAutocompleteService from "../../services/attribute_autocomplete.js"; -import noteAutocompleteService from "../../services/note_autocomplete.js"; +import noteAutocompleteService, { CreateMode } from "../../services/note_autocomplete.js"; import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js"; import NoteContextAwareWidget from "../note_context_aware_widget.js"; import SpacedUpdate from "../../services/spaced_update.js"; @@ -429,7 +429,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.$rowTargetNote = this.$widget.find(".attr-row-target-note"); this.$inputTargetNote = this.$widget.find(".attr-input-target-note"); - noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { allowCreatingNotes: true }).on("autocomplete:noteselected", (event, suggestion, dataset) => { + noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { createMode: CreateMode.CreateAndLink }).on("autocomplete:noteselected", (event, suggestion, dataset) => { if (!suggestion.notePath) { return false; } diff --git a/apps/client/src/widgets/collections/board/api.ts b/apps/client/src/widgets/collections/board/api.ts index af88f935e5..690b660f67 100644 --- a/apps/client/src/widgets/collections/board/api.ts +++ b/apps/client/src/widgets/collections/board/api.ts @@ -39,7 +39,7 @@ export default class BoardApi { const parentNotePath = this.parentNote.noteId; // Create a new note as a child of the parent note - const { note: newNote, branch: newBranch } = await note_create.createNote(parentNotePath, { + const { note: newNote, branch: newBranch } = await note_create.createNoteIntoPath(parentNotePath, { activate: false, title }); @@ -140,7 +140,7 @@ export default class BoardApi { column: string, relativeToBranchId: string, direction: "before" | "after") { - const { note, branch } = await note_create.createNote(this.parentNote.noteId, { + const { note, branch } = await note_create.createNoteIntoPath(this.parentNote.noteId, { activate: false, targetBranchId: relativeToBranchId, target: direction, diff --git a/apps/client/src/widgets/collections/table/columns.tsx b/apps/client/src/widgets/collections/table/columns.tsx index 74db6ddb71..60985cfe9c 100644 --- a/apps/client/src/widgets/collections/table/columns.tsx +++ b/apps/client/src/widgets/collections/table/columns.tsx @@ -6,6 +6,7 @@ import Icon from "../../react/Icon.jsx"; import { useEffect, useRef, useState } from "preact/hooks"; import froca from "../../../services/froca.js"; import NoteAutocomplete from "../../react/NoteAutocomplete.jsx"; +import { CreateMode } from "../../../services/note_autocomplete.js"; type ColumnType = LabelType | "relation"; @@ -227,7 +228,7 @@ function RelationEditor({ cell, success }: EditorOpts) { inputRef={inputRef} noteId={cell.getValue()} opts={{ - allowCreatingNotes: true, + createMode: CreateMode.CreateAndLink, hideAllButtons: true }} noteIdChanged={success} diff --git a/apps/client/src/widgets/collections/table/row_editing.ts b/apps/client/src/widgets/collections/table/row_editing.ts index 22ef0e7e48..026fc03b6e 100644 --- a/apps/client/src/widgets/collections/table/row_editing.ts +++ b/apps/client/src/widgets/collections/table/row_editing.ts @@ -19,7 +19,7 @@ export default function useRowTableEditing(api: RefObject, attributeD activate: false, ...customOpts } - note_create.createNote(notePath, opts).then(({ branch }) => { + note_create.createNoteIntoPath(notePath, opts).then(({ branch }) => { if (branch) { setTimeout(() => { if (!api.current) return; diff --git a/apps/client/src/widgets/dialogs/add_link.tsx b/apps/client/src/widgets/dialogs/add_link.tsx index 4bb1d1711c..b33cc64dca 100644 --- a/apps/client/src/widgets/dialogs/add_link.tsx +++ b/apps/client/src/widgets/dialogs/add_link.tsx @@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup"; import NoteAutocomplete from "../react/NoteAutocomplete"; import { useRef, useState, useEffect } from "preact/hooks"; import tree from "../../services/tree"; -import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"; +import note_autocomplete, { CreateMode, Suggestion } from "../../services/note_autocomplete"; import { logError } from "../../services/ws"; import FormGroup from "../react/FormGroup.js"; import { refToJQuerySelector } from "../react/react_utils"; @@ -133,7 +133,7 @@ export default function AddLinkDialog() { onChange={setSuggestion} opts={{ allowExternalLinks: true, - allowCreatingNotes: true + createMode: CreateMode.CreateAndLink, }} /> diff --git a/apps/client/src/widgets/dialogs/include_note.tsx b/apps/client/src/widgets/dialogs/include_note.tsx index aabd64bab7..4cc9413b90 100644 --- a/apps/client/src/widgets/dialogs/include_note.tsx +++ b/apps/client/src/widgets/dialogs/include_note.tsx @@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup"; import Modal from "../react/Modal"; import NoteAutocomplete from "../react/NoteAutocomplete"; import Button from "../react/Button"; -import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete"; +import { CreateMode, Suggestion, triggerRecentNotes } from "../../services/note_autocomplete"; import tree from "../../services/tree"; import froca from "../../services/froca"; import { useTriliumEvent } from "../react/hooks"; @@ -50,7 +50,7 @@ export default function IncludeNoteDialog() { inputRef={autoCompleteRef} opts={{ hideGoToSelectedNoteButton: true, - allowCreatingNotes: true + createMode: CreateMode.CreateOnly, }} /> diff --git a/apps/client/src/widgets/dialogs/jump_to_note.tsx b/apps/client/src/widgets/dialogs/jump_to_note.tsx index 89c4388039..c54e0b77ae 100644 --- a/apps/client/src/widgets/dialogs/jump_to_note.tsx +++ b/apps/client/src/widgets/dialogs/jump_to_note.tsx @@ -3,7 +3,7 @@ import Button from "../react/Button"; import NoteAutocomplete from "../react/NoteAutocomplete"; import { t } from "../../services/i18n"; import { useRef, useState } from "preact/hooks"; -import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"; +import note_autocomplete, { CreateMode, Suggestion } from "../../services/note_autocomplete"; import appContext from "../../components/app_context"; import commandRegistry from "../../services/command_registry"; import { refToJQuerySelector } from "../react/react_utils"; @@ -12,34 +12,57 @@ import shortcutService from "../../services/shortcuts"; const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120; -type Mode = "last-search" | "recent-notes" | "commands"; +enum Mode { + LastSearch, + RecentNotes, + Commands, +} export default function JumpToNoteDialogComponent() { const [ mode, setMode ] = useState(); const [ lastOpenedTs, setLastOpenedTs ] = useState(0); const containerRef = useRef(null); const autocompleteRef = useRef(null); - const [ isCommandMode, setIsCommandMode ] = useState(mode === "commands"); + const [ isCommandMode, setIsCommandMode ] = useState(mode === Mode.Commands); const [ initialText, setInitialText ] = useState(isCommandMode ? "> " : ""); const actualText = useRef(initialText); const [ shown, setShown ] = useState(false); - - async function openDialog(commandMode: boolean) { + + async function openDialog(requestedMode: Mode) { let newMode: Mode; let initialText = ""; - if (commandMode) { - newMode = "commands"; - initialText = ">"; - } else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) { - // if you open the Jump To dialog soon after using it previously, it can often mean that you - // actually want to search for the same thing (e.g., you opened the wrong note at first try) - // so we'll keep the content. - // if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead. - newMode = "last-search"; - initialText = actualText.current; - } else { - newMode = "recent-notes"; + switch (requestedMode) { + case Mode.Commands: + newMode = Mode.Commands; + initialText = ">"; + break; + + case Mode.LastSearch: + // if you open the Jump To dialog soon after using it previously, it can often mean that you + // actually want to search for the same thing (e.g., you opened the wrong note at first try) + // so we'll keep the content. + // if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead. + if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) { + newMode = Mode.LastSearch; + initialText = actualText.current; + } else { + newMode = Mode.RecentNotes; + } + break; + + // Mode.RecentNotes intentionally falls through to default: + // both represent the "normal open" behavior, where we decide between + // showing recent notes or restoring the last search depending on timing. + case Mode.RecentNotes: + default: + if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) { + newMode = Mode.LastSearch; + initialText = actualText.current; + } else { + newMode = Mode.RecentNotes; + } + break; } if (mode !== newMode) { @@ -51,14 +74,14 @@ export default function JumpToNoteDialogComponent() { setLastOpenedTs(Date.now()); } - useTriliumEvent("jumpToNote", () => openDialog(false)); - useTriliumEvent("commandPalette", () => openDialog(true)); + useTriliumEvent("jumpToNote", () => openDialog(Mode.RecentNotes)); + useTriliumEvent("commandPalette", () => openDialog(Mode.Commands)); async function onItemSelected(suggestion?: Suggestion | null) { if (!suggestion) { return; } - + setShown(false); if (suggestion.notePath) { appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath); @@ -83,7 +106,7 @@ export default function JumpToNoteDialogComponent() { $autoComplete .trigger("focus") .trigger("select"); - + // Add keyboard shortcut for full search shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => { if (!isCommandMode) { @@ -91,7 +114,7 @@ export default function JumpToNoteDialogComponent() { } }); } - + async function showInFullSearch() { try { setShown(false); @@ -116,7 +139,7 @@ export default function JumpToNoteDialogComponent() { container={containerRef} text={initialText} opts={{ - allowCreatingNotes: true, + createMode: CreateMode.CreateOnly, hideGoToSelectedNoteButton: true, allowJumpToSearchNotes: true, isCommandPalette: true @@ -129,9 +152,9 @@ export default function JumpToNoteDialogComponent() { />} onShown={onShown} onHidden={() => setShown(false)} - footer={!isCommandMode &&