mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 22:31:14 +08:00
feat(search): add create into inbox to search
This commit is contained in:
parent
a577fd45e2
commit
d32bfcc160
26 changed files with 621 additions and 294 deletions
|
|
@ -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<CreateChildrenResponse>(`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() }) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -233,7 +233,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({
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
||||
const isProtected = treeService.getParentProtectedStatus(this.node);
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
noteCreateService.createNoteIntoPath(parentNotePath, {
|
||||
target: "after",
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type: type,
|
||||
|
|
@ -283,7 +283,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||
} else if (command === "insertChildNote") {
|
||||
const parentNotePath = treeService.getNotePath(this.node);
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
noteCreateService.createNoteIntoPath(parentNotePath, {
|
||||
type: type,
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
|
|
|
|||
|
|
@ -5,6 +5,23 @@ import froca from "./froca.js";
|
|||
import { t } from "./i18n.js";
|
||||
import commandRegistry from "./command_registry.js";
|
||||
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
|
||||
import { MentionAction } from "@triliumnext/ckeditor5/src/augmentation.js";
|
||||
|
||||
/**
|
||||
* Extends CKEditor's MentionFeedObjectItem with extra fields used by Trilium.
|
||||
* These additional props (like action, notePath, name, etc.) carry note
|
||||
* metadata and legacy compatibility info needed for custom autocomplete
|
||||
* and link insertion behavior beyond CKEditor’s base mention support.
|
||||
*/
|
||||
type ExtendedMentionFeedObjectItem = MentionFeedObjectItem & {
|
||||
action?: string;
|
||||
noteTitle?: string;
|
||||
name?: string;
|
||||
link?: string;
|
||||
notePath?: string;
|
||||
parentNoteId?: string;
|
||||
highlightedNotePathTitle?: string;
|
||||
};
|
||||
|
||||
// this key needs to have this value, so it's hit by the tooltip
|
||||
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
||||
|
|
@ -23,14 +40,39 @@ function getSearchDelay(notesCount: number): number {
|
|||
}
|
||||
let searchDelay = getSearchDelay(notesCount);
|
||||
|
||||
// TODO: Deduplicate with server.
|
||||
// String values ensure stable, human-readable identifiers across serialization (JSON, CKEditor, logs).
|
||||
export enum SuggestionAction {
|
||||
// These values intentionally mirror MentionAction string values 1:1.
|
||||
// This overlap ensures that when a suggestion triggers a note creation callback,
|
||||
// the receiving features (e.g. note creation handlers, CKEditor mentions) can interpret
|
||||
// the action type consistently
|
||||
CreateNoteIntoInbox = MentionAction.CreateNoteIntoInbox,
|
||||
CreateNoteIntoPath = MentionAction.CreateNoteIntoPath,
|
||||
CreateAndLinkNoteIntoInbox = MentionAction.CreateAndLinkNoteIntoInbox,
|
||||
CreateAndLinkNoteIntoPath = MentionAction.CreateAndLinkNoteIntoPath,
|
||||
|
||||
SearchNotes = "search-notes",
|
||||
ExternalLink = "external-link",
|
||||
Command = "command",
|
||||
}
|
||||
|
||||
export enum CreateMode {
|
||||
None = "none",
|
||||
CreateOnly = "create-only",
|
||||
CreateAndLink = "create-and-link"
|
||||
}
|
||||
|
||||
// NOTE: Previously marked for deduplication with a server-side type,
|
||||
// but review on 2025-10-12 (using `rg Suggestion`) found no corresponding
|
||||
// server implementation.
|
||||
// This interface appears to be client-only.
|
||||
export interface Suggestion {
|
||||
noteTitle?: string;
|
||||
externalLink?: string;
|
||||
notePathTitle?: string;
|
||||
notePath?: string;
|
||||
highlightedNotePathTitle?: string;
|
||||
action?: string | "create-note" | "search-notes" | "external-link" | "command";
|
||||
action?: SuggestionAction;
|
||||
parentNoteId?: string;
|
||||
icon?: string;
|
||||
commandId?: string;
|
||||
|
|
@ -43,7 +85,7 @@ export interface Suggestion {
|
|||
export interface Options {
|
||||
container?: HTMLElement | null;
|
||||
fastSearch?: boolean;
|
||||
allowCreatingNotes?: boolean;
|
||||
createMode?: CreateMode;
|
||||
allowJumpToSearchNotes?: boolean;
|
||||
allowExternalLinks?: boolean;
|
||||
/** If set, hides the right-side button corresponding to go to selected note. */
|
||||
|
|
@ -54,110 +96,160 @@ export interface Options {
|
|||
isCommandPalette?: boolean;
|
||||
}
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
||||
return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
|
||||
async function autocompleteSourceForCKEditor(
|
||||
queryText: string,
|
||||
createMode: CreateMode
|
||||
): Promise<MentionFeedObjectItem[]> {
|
||||
// Wrap the callback-based autocompleteSource in a Promise for async/await
|
||||
const rows = await new Promise<Suggestion[]>((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<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
|
||||
let results = await server.get<Suggestion[]>(
|
||||
`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);
|
||||
}
|
||||
|
||||
if (length >= 1 && options.allowJumpToSearchNotes) {
|
||||
results = results.concat([
|
||||
{
|
||||
action: "search-notes",
|
||||
noteTitle: term,
|
||||
highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
|
||||
// --- 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 (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
|
||||
// --- Jump to Search Notes ---
|
||||
if (trimmedTerm.length >= 1 && options.allowJumpToSearchNotes) {
|
||||
results = [
|
||||
...results,
|
||||
{
|
||||
action: SuggestionAction.SearchNotes,
|
||||
noteTitle: trimmedTerm,
|
||||
highlightedNotePathTitle: `${t("note_autocomplete.search-for", {
|
||||
term: trimmedTerm,
|
||||
})} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// --- 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<HTMLElement>) {
|
||||
|
|
@ -198,6 +290,64 @@ function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
|
|||
$el.autocomplete("val", searchString);
|
||||
}
|
||||
|
||||
function renderCommandSuggestion(s: Suggestion): string {
|
||||
const icon = s.icon || "bx bx-terminal";
|
||||
const shortcut = s.commandShortcut
|
||||
? `<kbd class="command-shortcut">${s.commandShortcut}</kbd>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<div class="command-suggestion">
|
||||
<span class="command-icon ${icon}"></span>
|
||||
<div class="command-content">
|
||||
<div class="command-name">${s.highlightedNotePathTitle}</div>
|
||||
${s.commandDescription ? `<div class="command-description">${s.commandDescription}</div>` : ""}
|
||||
</div>
|
||||
${shortcut}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="note-suggestion ${actionClass}" style="display:inline-flex; align-items:center;">
|
||||
<span class="icon ${iconClass}" style="display:inline-block; vertical-align:middle; line-height:1; margin-right:0.4em;"></span>
|
||||
<span class="text" style="display:inline-block; vertical-align:middle;">
|
||||
<span class="search-result-title">${s.highlightedNotePathTitle}</span>
|
||||
${s.highlightedAttributeSnippet
|
||||
? `<span class="search-result-attributes">${s.highlightedAttributeSnippet}</span>`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSuggestion(suggestion: Suggestion): string {
|
||||
return suggestion.action === SuggestionAction.Command
|
||||
? renderCommandSuggestion(suggestion)
|
||||
: renderNoteSuggestion(suggestion);
|
||||
}
|
||||
|
||||
function initNoteAutocomplete($el: JQuery<HTMLElement>, 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<HTMLElement>, 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<HTMLElement>, options?: Options) {
|
|||
}
|
||||
},
|
||||
displayKey: "notePathTitle",
|
||||
templates: {
|
||||
suggestion: (suggestion) => {
|
||||
if (suggestion.action === "command") {
|
||||
let html = `<div class="command-suggestion">`;
|
||||
html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
|
||||
html += `<div class="command-content">`;
|
||||
html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`;
|
||||
if (suggestion.commandDescription) {
|
||||
html += `<div class="command-description">${suggestion.commandDescription}</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
if (suggestion.commandShortcut) {
|
||||
html += `<kbd class="command-shortcut">${suggestion.commandShortcut}</kbd>`;
|
||||
}
|
||||
html += '</div>';
|
||||
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 = `<div class="note-suggestion ${actionClass}">`;
|
||||
html += `<span class="icon ${iconClass}"></span>`;
|
||||
html += `<span class="text">`;
|
||||
html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
|
||||
|
||||
// Add attribute snippet inline if available
|
||||
if (suggestion.highlightedAttributeSnippet) {
|
||||
html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
|
||||
}
|
||||
|
||||
html += `</span>`;
|
||||
html += `</div>`;
|
||||
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;
|
||||
}
|
||||
|
||||
if (suggestion.action === "external-link") {
|
||||
$el.setSelectedNotePath(null);
|
||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||
|
||||
$el.autocomplete("val", suggestion.externalLink);
|
||||
|
||||
$el.autocomplete("close");
|
||||
|
||||
$el.trigger("autocomplete:externallinkselected", [suggestion]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (suggestion.action === "create-note") {
|
||||
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
|
||||
if (!success) {
|
||||
return;
|
||||
switch (suggestion.action) {
|
||||
case SuggestionAction.Command: {
|
||||
$el.autocomplete("close");
|
||||
$el.trigger("autocomplete:commandselected", [suggestion]);
|
||||
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);
|
||||
case SuggestionAction.ExternalLink: {
|
||||
$el.setSelectedNotePath(null);
|
||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||
$el.autocomplete("val", suggestion.externalLink);
|
||||
$el.autocomplete("close");
|
||||
$el.trigger("autocomplete:externallinkselected", [suggestion]);
|
||||
break;
|
||||
}
|
||||
|
||||
// --- CREATE NOTE INTO INBOX ---
|
||||
case SuggestionAction.CreateNoteIntoInbox: {
|
||||
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType();
|
||||
if (!success) return;
|
||||
|
||||
const { note } = await noteCreateService.createNoteIntoInbox({
|
||||
title: suggestion.noteTitle,
|
||||
activate: true,
|
||||
type: noteType,
|
||||
templateNoteId,
|
||||
});
|
||||
|
||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
||||
|
||||
$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,
|
||||
});
|
||||
|
||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
||||
|
||||
$el.trigger("autocomplete:noteselected", [suggestion]);
|
||||
$el.autocomplete("close");
|
||||
break;
|
||||
}
|
||||
|
||||
// --- 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,
|
||||
});
|
||||
|
||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
||||
|
||||
$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,
|
||||
});
|
||||
|
||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
||||
|
||||
$el.trigger("autocomplete:noteselected", [suggestion]);
|
||||
$el.autocomplete("close");
|
||||
break;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
if (suggestion.action === "search-notes") {
|
||||
const searchString = suggestion.noteTitle;
|
||||
appContext.triggerCommand("searchNotes", { searchString });
|
||||
return;
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath(suggestion.notePath);
|
||||
$el.setSelectedExternalLink(null);
|
||||
|
||||
$el.autocomplete("val", suggestion.noteTitle);
|
||||
|
||||
$el.autocomplete("close");
|
||||
|
||||
$el.trigger("autocomplete:noteselected", [suggestion]);
|
||||
});
|
||||
|
||||
$el.on("autocomplete:closed", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1883,7 +1883,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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,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
|
||||
});
|
||||
|
|
@ -130,7 +130,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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default function useRowTableEditing(api: RefObject<Tabulator>, 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;
|
||||
|
|
|
|||
|
|
@ -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 { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
||||
import { logError } from "../../services/ws";
|
||||
import FormGroup from "../react/FormGroup.js";
|
||||
|
|
@ -131,7 +131,7 @@ export default function AddLinkDialog() {
|
|||
onChange={setSuggestion}
|
||||
opts={{
|
||||
allowExternalLinks: true,
|
||||
allowCreatingNotes: true
|
||||
createMode: CreateMode.CreateAndLink,
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
|||
|
|
@ -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 EditableTextTypeWidget, { type BoxSize } from "../type_widgets/editable_text";
|
||||
|
|
@ -49,7 +49,7 @@ export default function IncludeNoteDialog() {
|
|||
inputRef={autoCompleteRef}
|
||||
opts={{
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true
|
||||
createMode: CreateMode.CreateOnly,
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
@ -83,4 +83,4 @@ async function includeNote(notePath: string, textTypeWidget: EditableTextTypeWid
|
|||
} else {
|
||||
textTypeWidget.addIncludeNote(noteId, boxSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Mode>();
|
||||
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const autocompleteRef = useRef<HTMLInputElement>(null);
|
||||
const [ isCommandMode, setIsCommandMode ] = useState(mode === "commands");
|
||||
const [ isCommandMode, setIsCommandMode ] = useState(mode === Mode.Commands);
|
||||
const [ initialText, setInitialText ] = useState(isCommandMode ? "> " : "");
|
||||
const actualText = useRef<string>(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 && <Button
|
||||
className="show-in-full-text-button"
|
||||
text={t("jump_to_note.search_button")}
|
||||
footer={!isCommandMode && <Button
|
||||
className="show-in-full-text-button"
|
||||
text={t("jump_to_note.search_button")}
|
||||
keyboardShortcut="Ctrl+Enter"
|
||||
onClick={showInFullSearch}
|
||||
/>}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { useEffect, useState } from "preact/hooks";
|
|||
import note_types from "../../services/note_types";
|
||||
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
||||
import { TreeCommandNames } from "../../menus/tree_context_menu";
|
||||
import { Suggestion } from "../../services/note_autocomplete";
|
||||
import { CreateMode, Suggestion } from "../../services/note_autocomplete";
|
||||
import Badge from "../react/Badge";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ export default function NoteTypeChooserDialogComponent() {
|
|||
onChange={setParentNote}
|
||||
placeholder={t("note_type_chooser.search_placeholder")}
|
||||
opts={{
|
||||
allowCreatingNotes: false,
|
||||
createMode: CreateMode.None,
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowJumpToSearchNotes: false,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import BasicWidget from "../basic_widget.js";
|
|||
import toastService from "../../services/toast.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import server from "../../services/server.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import noteAutocompleteService, { CreateMode } from "../../services/note_autocomplete.js";
|
||||
|
||||
import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
|
||||
import { formatMarkdown } from "./utils.js";
|
||||
|
|
@ -163,7 +163,7 @@ export default class LlmChatPanel extends BasicWidget {
|
|||
const mentionSetup: MentionFeed[] = [
|
||||
{
|
||||
marker: "@",
|
||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, CreateMode.CreateAndLink),
|
||||
itemRenderer: (item) => {
|
||||
const suggestion = item as Suggestion;
|
||||
const itemElement = document.createElement("button");
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function MobileDetailMenu() {
|
|||
],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
note_create.createNoteIntoPath(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
|||
// without await as this otherwise causes deadlock through component mutex
|
||||
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (this.noteContext && parentNotePath) {
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
noteCreateService.createNoteIntoPath(parentNotePath, {
|
||||
isProtected: note.isProtected,
|
||||
saveSelection: true,
|
||||
textEditor: await this.noteContext.getTextEditor()
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||
} else if (target.classList.contains("add-note-button")) {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
const parentNotePath = treeService.getNotePath(node);
|
||||
noteCreateService.createNote(parentNotePath, { isProtected: node.data.isProtected });
|
||||
noteCreateService.createNoteIntoPath(parentNotePath, { isProtected: node.data.isProtected });
|
||||
} else if (target.classList.contains("enter-workspace-button")) {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
this.triggerCommand("hoistNote", { noteId: node.data.noteId });
|
||||
|
|
@ -1843,7 +1843,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||
const node = this.getActiveNode();
|
||||
if (!node) return;
|
||||
const notePath = treeService.getNotePath(node);
|
||||
noteCreateService.createNote(notePath, {
|
||||
noteCreateService.createNoteIntoPath(notePath, {
|
||||
isProtected: node.data.isProtected
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { t } from "../services/i18n.js";
|
|||
import server from "../services/server.js";
|
||||
import ws from "../services/ws.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||
import noteAutocompleteService, { CreateMode } from "../services/note_autocomplete.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import options from "../services/options.js";
|
||||
|
|
@ -341,7 +341,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||
|
||||
if (utils.isDesktop()) {
|
||||
// no need to wait for this
|
||||
noteAutocompleteService.initNoteAutocomplete($input, { allowCreatingNotes: true });
|
||||
noteAutocompleteService.initNoteAutocomplete($input, { createMode: CreateMode.CreateOnly});
|
||||
|
||||
$input.on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
||||
this.promotedAttributeChanged(event);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState }
|
|||
import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
|
||||
import note_autocomplete, { CreateMode, Suggestion } from "../../../services/note_autocomplete";
|
||||
import CKEditor, { CKEditorApi } from "../../react/CKEditor";
|
||||
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
||||
import FAttribute from "../../../entities/fattribute";
|
||||
|
|
@ -20,6 +20,7 @@ import type { CommandData, FilteredCommandNames } from "../../../components/app_
|
|||
import { AttributeType } from "@triliumnext/commons";
|
||||
import attributes from "../../../services/attributes";
|
||||
import note_create from "../../../services/note_create";
|
||||
import { MentionAction } from "@triliumnext/ckeditor5/src/augmentation.js";
|
||||
|
||||
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ const HELP_TEXT = `
|
|||
const mentionSetup: MentionFeed[] = [
|
||||
{
|
||||
marker: "@",
|
||||
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText),
|
||||
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText, CreateMode.CreateAndLink),
|
||||
itemRenderer: (_item) => {
|
||||
const item = _item as Suggestion;
|
||||
const itemElement = document.createElement("button");
|
||||
|
|
@ -252,17 +253,40 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||
|
||||
$el.text(title);
|
||||
},
|
||||
createNoteForReferenceLink: async (title: string) => {
|
||||
let result;
|
||||
if (notePath) {
|
||||
result = await note_create.createNoteWithTypePrompt(notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
createNoteFromCkEditor: async (
|
||||
title: string,
|
||||
parentNotePath: string | undefined,
|
||||
action: MentionAction
|
||||
): Promise<string> => {
|
||||
if (!parentNotePath) {
|
||||
console.warn("Missing parentNotePath in createNoteFromCkEditor()");
|
||||
return "";
|
||||
}
|
||||
|
||||
return result?.note?.getBestNotePathString();
|
||||
switch (action) {
|
||||
case MentionAction.CreateNoteIntoInbox:
|
||||
case MentionAction.CreateAndLinkNoteIntoInbox: {
|
||||
const { note } = await note_create.createNoteIntoInbox({
|
||||
title,
|
||||
activate: false
|
||||
});
|
||||
return note?.getBestNotePathString() ?? "";
|
||||
}
|
||||
|
||||
case MentionAction.CreateNoteIntoPath:
|
||||
case MentionAction.CreateAndLinkNoteIntoPath: {
|
||||
const resp = await note_create.createNoteIntoPathWithTypePrompt(parentNotePath, {
|
||||
title,
|
||||
activate: false
|
||||
});
|
||||
return resp?.note?.getBestNotePathString() ?? "";
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Unknown MentionAction:", action);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}), [ notePath ]));
|
||||
|
||||
// Keyboard shortcuts
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
|||
import getTemplates from "./snippets.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import { getMermaidConfig } from "../../../services/mermaid.js";
|
||||
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
|
||||
import noteAutocompleteService, { CreateMode, type Suggestion } from "../../../services/note_autocomplete.js";
|
||||
import mimeTypesService from "../../../services/mime_types.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import { buildToolbarConfig } from "./toolbar.js";
|
||||
|
|
@ -181,7 +181,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
|||
feeds: [
|
||||
{
|
||||
marker: "@",
|
||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, CreateMode.CreateAndLink),
|
||||
itemRenderer: (item) => {
|
||||
const itemElement = document.createElement("button");
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { buildConfig, BuildEditorOptions, OPEN_SOURCE_LICENSE_KEY } from "./cked
|
|||
import type FNote from "../../entities/fnote.js";
|
||||
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5";
|
||||
import { updateTemplateCache } from "./ckeditor/snippets.js";
|
||||
import { MentionAction } from "@triliumnext/ckeditor5/src/augmentation.js";
|
||||
import note_create from "../../services/note_create.js";
|
||||
|
||||
export type BoxSize = "small" | "medium" | "full";
|
||||
|
||||
|
|
@ -491,21 +493,81 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||
});
|
||||
}
|
||||
|
||||
async createNoteForReferenceLink(title: string) {
|
||||
if (!this.notePath) {
|
||||
return;
|
||||
async createNoteFromCkEditor (
|
||||
title: string,
|
||||
parentNotePath: string | undefined,
|
||||
action: MentionAction
|
||||
): Promise<string> {
|
||||
try {
|
||||
switch (action) {
|
||||
// --- Create note INTO inbox ---
|
||||
case MentionAction.CreateNoteIntoInbox: {
|
||||
const { success, noteType, templateNoteId } = await note_create.chooseNoteType();
|
||||
if (!success) return "";
|
||||
|
||||
const { note } = await note_create.createNoteIntoInbox({
|
||||
title,
|
||||
activate: true,
|
||||
type: noteType,
|
||||
templateNoteId,
|
||||
});
|
||||
|
||||
return note?.getBestNotePathString() ?? "";
|
||||
}
|
||||
|
||||
// --- Create note INTO current path ---
|
||||
case MentionAction.CreateNoteIntoPath: {
|
||||
const { success, noteType, templateNoteId, notePath } = await note_create.chooseNoteType();
|
||||
if (!success) return "";
|
||||
|
||||
const { note } = await note_create.createNoteIntoPath(notePath || parentNotePath, {
|
||||
title,
|
||||
activate: true,
|
||||
type: noteType,
|
||||
templateNoteId,
|
||||
});
|
||||
|
||||
return note?.getBestNotePathString() ?? "";
|
||||
}
|
||||
|
||||
// --- Create & link note INTO inbox ---
|
||||
case MentionAction.CreateAndLinkNoteIntoInbox: {
|
||||
const { success, noteType, templateNoteId } = await note_create.chooseNoteType();
|
||||
if (!success) return "";
|
||||
|
||||
const { note } = await note_create.createNoteIntoInbox({
|
||||
title,
|
||||
activate: false,
|
||||
type: noteType,
|
||||
templateNoteId,
|
||||
});
|
||||
|
||||
return note?.getBestNotePathString() ?? "";
|
||||
}
|
||||
|
||||
// --- Create & link note INTO current path ---
|
||||
case MentionAction.CreateAndLinkNoteIntoPath: {
|
||||
const { success, noteType, templateNoteId, notePath } = await note_create.chooseNoteType();
|
||||
if (!success) return "";
|
||||
|
||||
const { note } = await note_create.createNoteIntoPath(notePath || parentNotePath, {
|
||||
title,
|
||||
activate: false,
|
||||
type: noteType,
|
||||
templateNoteId,
|
||||
});
|
||||
|
||||
return note?.getBestNotePathString() ?? "";
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Unknown MentionAction:", action);
|
||||
return "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error while creating note from CKEditor:", err);
|
||||
return "";
|
||||
}
|
||||
|
||||
const resp = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
|
||||
if (!resp || !resp.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
return resp.note.getBestNotePathString();
|
||||
}
|
||||
|
||||
async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import noteAutocompleteService, { CreateMode } from "../../services/note_autocomplete.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import searchService from "../../services/search.js";
|
||||
|
|
@ -77,7 +77,7 @@ export default class EmptyTypeWidget extends TypeWidget {
|
|||
noteAutocompleteService
|
||||
.initNoteAutocomplete(this.$autoComplete, {
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true,
|
||||
createMode: CreateMode.CreateOnly,
|
||||
allowJumpToSearchNotes: true,
|
||||
container: this.$results[0]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import "ckeditor5";
|
||||
import { CKTextEditor } from "src";
|
||||
|
||||
export enum MentionAction {
|
||||
CreateNoteIntoInbox = "create-note-into-inbox",
|
||||
CreateNoteIntoPath = "create-note-into-path",
|
||||
CreateAndLinkNoteIntoInbox = "create-and-link-note-into-inbox",
|
||||
CreateAndLinkNoteIntoPath = "create-and-link-note-into-path"
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Component {
|
||||
|
|
@ -7,7 +15,8 @@ declare global {
|
|||
|
||||
interface EditorComponent extends Component {
|
||||
loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string): Promise<void>;
|
||||
createNoteForReferenceLink(title: string): Promise<string>;
|
||||
// Must Return Note Path
|
||||
createNoteFromCkEditor(title: string, parentNotePath: string | undefined, action: MentionAction): Promise<string>;
|
||||
loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>): void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "ckeditor5";
|
||||
import { MentionAction } from "../augmentation";
|
||||
|
||||
/**
|
||||
* Overrides the actions taken by the Mentions plugin (triggered by `@` in the text editor, or `~` & `#` in the attribute editor):
|
||||
|
|
@ -9,11 +10,11 @@ import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "cked
|
|||
*/
|
||||
export default class MentionCustomization extends Plugin {
|
||||
|
||||
static get requires() {
|
||||
static get requires() {
|
||||
return [ Mention ];
|
||||
}
|
||||
|
||||
public static get pluginName() {
|
||||
public static get pluginName() {
|
||||
return "MentionCustomization" as const;
|
||||
}
|
||||
|
||||
|
|
@ -25,20 +26,21 @@ export default class MentionCustomization extends Plugin {
|
|||
}
|
||||
|
||||
interface MentionOpts {
|
||||
mention: string | {
|
||||
id: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
marker: string;
|
||||
text?: string;
|
||||
range?: ModelRange;
|
||||
mention: string | {
|
||||
id: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
marker: string;
|
||||
text?: string;
|
||||
range?: ModelRange;
|
||||
}
|
||||
|
||||
interface MentionAttribute {
|
||||
id: string;
|
||||
action?: "create-note";
|
||||
noteTitle: string;
|
||||
notePath: string;
|
||||
id: string;
|
||||
action?: MentionAction;
|
||||
noteTitle: string;
|
||||
notePath: string;
|
||||
parentNoteId?: string;
|
||||
}
|
||||
|
||||
class CustomMentionCommand extends Command {
|
||||
|
|
@ -56,14 +58,27 @@ class CustomMentionCommand extends Command {
|
|||
model.insertContent( writer.createText( mention.id, {} ), range );
|
||||
});
|
||||
}
|
||||
else if (mention.action === 'create-note') {
|
||||
const editorEl = this.editor.editing.view.getDomRoot();
|
||||
const component = glob.getComponentByEl<EditorComponent>(editorEl);
|
||||
else if (
|
||||
mention.action === MentionAction.CreateNoteIntoInbox ||
|
||||
mention.action === MentionAction.CreateNoteIntoPath ||
|
||||
mention.action === MentionAction.CreateAndLinkNoteIntoInbox ||
|
||||
mention.action === MentionAction.CreateAndLinkNoteIntoPath
|
||||
) {
|
||||
const editorEl = this.editor.editing.view.getDomRoot();
|
||||
const component = glob.getComponentByEl<EditorComponent>(editorEl);
|
||||
|
||||
component.createNoteForReferenceLink(mention.noteTitle).then(notePath => {
|
||||
this.insertReference(range, notePath);
|
||||
});
|
||||
}
|
||||
// use parentNoteId as fallback when notePath is missing
|
||||
const parentNotePath = mention.notePath || mention.parentNoteId;
|
||||
|
||||
component
|
||||
.createNoteFromCkEditor(mention.noteTitle, parentNotePath, mention.action)
|
||||
.then(notePath => {
|
||||
if (notePath) {
|
||||
this.insertReference(range, notePath);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error("Error creating note from CKEditor mention:", err));
|
||||
}
|
||||
else {
|
||||
this.insertReference(range, mention.notePath);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue