mirror of
https://github.com/zadam/trilium.git
synced 2025-11-12 23:01:05 +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 linkService from "../services/link.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||||
|
import noteCreateService from "../services/note_create";
|
||||||
|
|
||||||
export default class Entrypoints extends Component {
|
export default class Entrypoints extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -24,23 +25,7 @@ export default class Entrypoints extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteIntoInboxCommand() {
|
async createNoteIntoInboxCommand() {
|
||||||
const inboxNote = await dateNoteService.getInboxNote();
|
await noteCreateService.createNoteIntoInbox();
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
|
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export default class MainTreeExecutors extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(activeNoteContext.notePath, {
|
await noteCreateService.createNoteIntoPath(activeNoteContext.notePath, {
|
||||||
isProtected: activeNoteContext.note.isProtected,
|
isProtected: activeNoteContext.note.isProtected,
|
||||||
saveSelection: false
|
saveSelection: false
|
||||||
});
|
});
|
||||||
|
|
@ -72,7 +72,7 @@ export default class MainTreeExecutors extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(parentNotePath, {
|
await noteCreateService.createNoteIntoPath(parentNotePath, {
|
||||||
target: "after",
|
target: "after",
|
||||||
targetBranchId: node.data.branchId,
|
targetBranchId: node.data.branchId,
|
||||||
isProtected: isProtected,
|
isProtected: isProtected,
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ export default class RootCommandExecutor extends Component {
|
||||||
// Create a new AI Chat note at the root level
|
// Create a new AI Chat note at the root level
|
||||||
const rootNoteId = "root";
|
const rootNoteId = "root";
|
||||||
|
|
||||||
const result = await noteCreateService.createNote(rootNoteId, {
|
const result = await noteCreateService.createNoteIntoPath(rootNoteId, {
|
||||||
title: "New AI Chat",
|
title: "New AI Chat",
|
||||||
type: "aiChat",
|
type: "aiChat",
|
||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||||
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
||||||
const isProtected = treeService.getParentProtectedStatus(this.node);
|
const isProtected = treeService.getParentProtectedStatus(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNotePath, {
|
noteCreateService.createNoteIntoPath(parentNotePath, {
|
||||||
target: "after",
|
target: "after",
|
||||||
targetBranchId: this.node.data.branchId,
|
targetBranchId: this.node.data.branchId,
|
||||||
type: type,
|
type: type,
|
||||||
|
|
@ -283,7 +283,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||||
} else if (command === "insertChildNote") {
|
} else if (command === "insertChildNote") {
|
||||||
const parentNotePath = treeService.getNotePath(this.node);
|
const parentNotePath = treeService.getNotePath(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNotePath, {
|
noteCreateService.createNoteIntoPath(parentNotePath, {
|
||||||
type: type,
|
type: type,
|
||||||
isProtected: this.node.data.isProtected,
|
isProtected: this.node.data.isProtected,
|
||||||
templateNoteId: templateNoteId
|
templateNoteId: templateNoteId
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,23 @@ import froca from "./froca.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
import commandRegistry from "./command_registry.js";
|
import commandRegistry from "./command_registry.js";
|
||||||
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
|
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
|
// this key needs to have this value, so it's hit by the tooltip
|
||||||
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
||||||
|
|
@ -23,14 +40,39 @@ function getSearchDelay(notesCount: number): number {
|
||||||
}
|
}
|
||||||
let searchDelay = getSearchDelay(notesCount);
|
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 {
|
export interface Suggestion {
|
||||||
noteTitle?: string;
|
noteTitle?: string;
|
||||||
externalLink?: string;
|
externalLink?: string;
|
||||||
notePathTitle?: string;
|
notePathTitle?: string;
|
||||||
notePath?: string;
|
notePath?: string;
|
||||||
highlightedNotePathTitle?: string;
|
highlightedNotePathTitle?: string;
|
||||||
action?: string | "create-note" | "search-notes" | "external-link" | "command";
|
action?: SuggestionAction;
|
||||||
parentNoteId?: string;
|
parentNoteId?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
commandId?: string;
|
commandId?: string;
|
||||||
|
|
@ -43,7 +85,7 @@ export interface Suggestion {
|
||||||
export interface Options {
|
export interface Options {
|
||||||
container?: HTMLElement | null;
|
container?: HTMLElement | null;
|
||||||
fastSearch?: boolean;
|
fastSearch?: boolean;
|
||||||
allowCreatingNotes?: boolean;
|
createMode?: CreateMode;
|
||||||
allowJumpToSearchNotes?: boolean;
|
allowJumpToSearchNotes?: boolean;
|
||||||
allowExternalLinks?: boolean;
|
allowExternalLinks?: boolean;
|
||||||
/** If set, hides the right-side button corresponding to go to selected note. */
|
/** If set, hides the right-side button corresponding to go to selected note. */
|
||||||
|
|
@ -54,110 +96,160 @@ export interface Options {
|
||||||
isCommandPalette?: boolean;
|
isCommandPalette?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
async function autocompleteSourceForCKEditor(
|
||||||
return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
|
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(
|
autocompleteSource(
|
||||||
queryText,
|
queryText,
|
||||||
(rows) => {
|
(suggestions) => resolve(suggestions),
|
||||||
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
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
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
|
// Check if we're in command mode
|
||||||
if (options.isCommandPalette && term.startsWith(">")) {
|
if (options.isCommandPalette && term.startsWith(">")) {
|
||||||
const commandQuery = term.substring(1).trim();
|
const commandQuery = term.substring(1).trim();
|
||||||
|
|
||||||
// Get commands (all if no query, filtered if query provided)
|
// Get commands (all if no query, filtered if query provided)
|
||||||
const commands = commandQuery.length === 0
|
const commands =
|
||||||
? commandRegistry.getAllCommands()
|
commandQuery.length === 0
|
||||||
: commandRegistry.searchCommands(commandQuery);
|
? commandRegistry.getAllCommands()
|
||||||
|
: commandRegistry.searchCommands(commandQuery);
|
||||||
|
|
||||||
// Convert commands to suggestions
|
// Convert commands to suggestions
|
||||||
const commandSuggestions: Suggestion[] = commands.map(cmd => ({
|
const commandSuggestions: Suggestion[] = commands.map((cmd) => ({
|
||||||
action: "command",
|
action: SuggestionAction.Command,
|
||||||
commandId: cmd.id,
|
commandId: cmd.id,
|
||||||
noteTitle: cmd.name,
|
noteTitle: cmd.name,
|
||||||
notePathTitle: `>${cmd.name}`,
|
notePathTitle: `>${cmd.name}`,
|
||||||
highlightedNotePathTitle: cmd.name,
|
highlightedNotePathTitle: cmd.name,
|
||||||
commandDescription: cmd.description,
|
commandDescription: cmd.description,
|
||||||
commandShortcut: cmd.shortcut,
|
commandShortcut: cmd.shortcut,
|
||||||
icon: cmd.icon
|
icon: cmd.icon,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cb(commandSuggestions);
|
callback(commandSuggestions);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fastSearch = options.fastSearch === false ? false : true;
|
const fastSearch = options.fastSearch !== false;
|
||||||
if (fastSearch === false) {
|
const trimmedTerm = term.trim();
|
||||||
if (term.trim().length === 0) {
|
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
|
||||||
return;
|
|
||||||
}
|
if (!fastSearch && trimmedTerm.length === 0) return;
|
||||||
cb([
|
|
||||||
|
if (!fastSearch) {
|
||||||
|
callback([
|
||||||
{
|
{
|
||||||
noteTitle: term,
|
noteTitle: trimmedTerm,
|
||||||
highlightedNotePathTitle: t("quick-search.searching")
|
highlightedNotePathTitle: t("quick-search.searching"),
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
|
let results = await server.get<Suggestion[]>(
|
||||||
const length = term.trim().length;
|
`autocomplete?query=${encodeURIComponent(trimmedTerm)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`
|
||||||
|
);
|
||||||
let results = await server.get<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
|
|
||||||
|
|
||||||
options.fastSearch = true;
|
options.fastSearch = true;
|
||||||
|
|
||||||
if (length >= 1 && options.allowCreatingNotes) {
|
// --- Create Note suggestions ---
|
||||||
results = [
|
if (trimmedTerm.length >= 1) {
|
||||||
{
|
switch (options.createMode) {
|
||||||
action: "create-note",
|
case CreateMode.CreateOnly: {
|
||||||
noteTitle: term,
|
results = [
|
||||||
parentNoteId: activeNoteId || "root",
|
{
|
||||||
highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
|
action: SuggestionAction.CreateNoteIntoInbox,
|
||||||
} as Suggestion
|
noteTitle: trimmedTerm,
|
||||||
].concat(results);
|
parentNoteId: "inbox",
|
||||||
}
|
highlightedNotePathTitle: t("note_autocomplete.create-note-into-inbox", { term: trimmedTerm }),
|
||||||
|
},
|
||||||
if (length >= 1 && options.allowJumpToSearchNotes) {
|
{
|
||||||
results = results.concat([
|
action: SuggestionAction.CreateNoteIntoPath,
|
||||||
{
|
noteTitle: trimmedTerm,
|
||||||
action: "search-notes",
|
parentNoteId: activeNoteId || "root",
|
||||||
noteTitle: term,
|
highlightedNotePathTitle: t("note_autocomplete.create-note-into-path", { term: trimmedTerm }),
|
||||||
highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
|
},
|
||||||
|
...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 = [
|
results = [
|
||||||
{
|
{
|
||||||
action: "external-link",
|
action: SuggestionAction.ExternalLink,
|
||||||
externalLink: term,
|
externalLink: trimmedTerm,
|
||||||
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term })
|
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term: trimmedTerm }),
|
||||||
} as Suggestion
|
},
|
||||||
].concat(results);
|
...results,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(results);
|
callback(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearText($el: JQuery<HTMLElement>) {
|
function clearText($el: JQuery<HTMLElement>) {
|
||||||
|
|
@ -198,6 +290,64 @@ function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
|
||||||
$el.autocomplete("val", searchString);
|
$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) {
|
function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||||
if ($el.hasClass("note-autocomplete-input")) {
|
if ($el.hasClass("note-autocomplete-input")) {
|
||||||
// clear any event listener added in previous invocation of this function
|
// clear any event listener added in previous invocation of this function
|
||||||
|
|
@ -283,24 +433,21 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||||
$el.autocomplete(
|
$el.autocomplete(
|
||||||
{
|
{
|
||||||
...autocompleteOptions,
|
...autocompleteOptions,
|
||||||
appendTo: document.querySelector("body"),
|
appendTo: document.body,
|
||||||
hint: false,
|
hint: false,
|
||||||
autoselect: true,
|
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,
|
openOnFocus: false,
|
||||||
minLength: 0,
|
minLength: 0,
|
||||||
tabAutocomplete: false
|
tabAutocomplete: false,
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
source: (term, cb) => {
|
source: (term, callback) => {
|
||||||
clearTimeout(debounceTimeoutId);
|
clearTimeout(debounceTimeoutId);
|
||||||
debounceTimeoutId = setTimeout(() => {
|
debounceTimeoutId = setTimeout(() => {
|
||||||
if (isComposingInput) {
|
if (!isComposingInput) {
|
||||||
return;
|
autocompleteSource(term, callback, options);
|
||||||
}
|
}
|
||||||
autocompleteSource(term, cb, options);
|
|
||||||
}, searchDelay);
|
}, searchDelay);
|
||||||
|
|
||||||
if (searchDelay === 0) {
|
if (searchDelay === 0) {
|
||||||
|
|
@ -308,109 +455,124 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
displayKey: "notePathTitle",
|
displayKey: "notePathTitle",
|
||||||
templates: {
|
templates: { suggestion: renderSuggestion },
|
||||||
suggestion: (suggestion) => {
|
cache: false,
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
|
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
|
||||||
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
|
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
|
||||||
if (suggestion.action === "command") {
|
switch (suggestion.action) {
|
||||||
$el.autocomplete("close");
|
case SuggestionAction.Command: {
|
||||||
$el.trigger("autocomplete:commandselected", [suggestion]);
|
$el.autocomplete("close");
|
||||||
return;
|
$el.trigger("autocomplete:commandselected", [suggestion]);
|
||||||
}
|
break;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
|
|
||||||
title: suggestion.noteTitle,
|
|
||||||
activate: false,
|
|
||||||
type: noteType,
|
|
||||||
templateNoteId: templateNoteId
|
|
||||||
});
|
|
||||||
|
|
||||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
case SuggestionAction.ExternalLink: {
|
||||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
$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", () => {
|
$el.on("autocomplete:closed", () => {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import type FNote from "../entities/fnote.js";
|
||||||
import type FBranch from "../entities/fbranch.js";
|
import type FBranch from "../entities/fbranch.js";
|
||||||
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
||||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
|
import dateNoteService from "../services/date_notes.js";
|
||||||
|
import { CreateChildrenResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
export interface CreateNoteOpts {
|
export interface CreateNoteOpts {
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
|
|
@ -37,7 +39,47 @@ interface DuplicateResponse {
|
||||||
note: FNote;
|
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(
|
options = Object.assign(
|
||||||
{
|
{
|
||||||
activate: true,
|
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();
|
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|
@ -123,7 +165,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN
|
||||||
options.type = noteType;
|
options.type = noteType;
|
||||||
options.templateNoteId = templateNoteId;
|
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. */
|
/* 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 {
|
export default {
|
||||||
createNote,
|
createNoteIntoInbox,
|
||||||
createNoteWithTypePrompt,
|
createNoteIntoPath,
|
||||||
|
createNoteIntoPathWithTypePrompt,
|
||||||
duplicateSubtree,
|
duplicateSubtree,
|
||||||
chooseNoteType
|
chooseNoteType
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1883,7 +1883,10 @@
|
||||||
},
|
},
|
||||||
"note_autocomplete": {
|
"note_autocomplete": {
|
||||||
"search-for": "Search for \"{{term}}\"",
|
"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}}\"",
|
"insert-external-link": "Insert external link to \"{{term}}\"",
|
||||||
"clear-text-field": "Clear text field",
|
"clear-text-field": "Clear text field",
|
||||||
"show-recent-notes": "Show recent notes",
|
"show-recent-notes": "Show recent notes",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import server from "../../services/server.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import linkService from "../../services/link.js";
|
import linkService from "../../services/link.js";
|
||||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.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 promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
|
||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||||
import SpacedUpdate from "../../services/spaced_update.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.$rowTargetNote = this.$widget.find(".attr-row-target-note");
|
||||||
this.$inputTargetNote = this.$widget.find(".attr-input-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) {
|
if (!suggestion.notePath) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default class BoardApi {
|
||||||
const parentNotePath = this.parentNote.noteId;
|
const parentNotePath = this.parentNote.noteId;
|
||||||
|
|
||||||
// Create a new note as a child of the parent note
|
// 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,
|
activate: false,
|
||||||
title
|
title
|
||||||
});
|
});
|
||||||
|
|
@ -130,7 +130,7 @@ export default class BoardApi {
|
||||||
column: string,
|
column: string,
|
||||||
relativeToBranchId: string,
|
relativeToBranchId: string,
|
||||||
direction: "before" | "after") {
|
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,
|
activate: false,
|
||||||
targetBranchId: relativeToBranchId,
|
targetBranchId: relativeToBranchId,
|
||||||
target: direction,
|
target: direction,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import Icon from "../../react/Icon.jsx";
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import froca from "../../../services/froca.js";
|
import froca from "../../../services/froca.js";
|
||||||
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
|
||||||
|
import { CreateMode } from "../../../services/note_autocomplete.js";
|
||||||
|
|
||||||
type ColumnType = LabelType | "relation";
|
type ColumnType = LabelType | "relation";
|
||||||
|
|
||||||
|
|
@ -227,7 +228,7 @@ function RelationEditor({ cell, success }: EditorOpts) {
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
noteId={cell.getValue()}
|
noteId={cell.getValue()}
|
||||||
opts={{
|
opts={{
|
||||||
allowCreatingNotes: true,
|
createMode: CreateMode.CreateAndLink,
|
||||||
hideAllButtons: true
|
hideAllButtons: true
|
||||||
}}
|
}}
|
||||||
noteIdChanged={success}
|
noteIdChanged={success}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default function useRowTableEditing(api: RefObject<Tabulator>, attributeD
|
||||||
activate: false,
|
activate: false,
|
||||||
...customOpts
|
...customOpts
|
||||||
}
|
}
|
||||||
note_create.createNote(notePath, opts).then(({ branch }) => {
|
note_create.createNoteIntoPath(notePath, opts).then(({ branch }) => {
|
||||||
if (branch) {
|
if (branch) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!api.current) return;
|
if (!api.current) return;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import { useRef, useState, useEffect } from "preact/hooks";
|
import { useRef, useState, useEffect } from "preact/hooks";
|
||||||
import tree from "../../services/tree";
|
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 { default as TextTypeWidget } from "../type_widgets/editable_text.js";
|
||||||
import { logError } from "../../services/ws";
|
import { logError } from "../../services/ws";
|
||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
|
|
@ -131,7 +131,7 @@ export default function AddLinkDialog() {
|
||||||
onChange={setSuggestion}
|
onChange={setSuggestion}
|
||||||
opts={{
|
opts={{
|
||||||
allowExternalLinks: true,
|
allowExternalLinks: true,
|
||||||
allowCreatingNotes: true
|
createMode: CreateMode.CreateAndLink,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import Button from "../react/Button";
|
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 tree from "../../services/tree";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import EditableTextTypeWidget, { type BoxSize } from "../type_widgets/editable_text";
|
import EditableTextTypeWidget, { type BoxSize } from "../type_widgets/editable_text";
|
||||||
|
|
@ -49,7 +49,7 @@ export default function IncludeNoteDialog() {
|
||||||
inputRef={autoCompleteRef}
|
inputRef={autoCompleteRef}
|
||||||
opts={{
|
opts={{
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowCreatingNotes: true
|
createMode: CreateMode.CreateOnly,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Button from "../react/Button";
|
||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { useRef, useState } from "preact/hooks";
|
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 appContext from "../../components/app_context";
|
||||||
import commandRegistry from "../../services/command_registry";
|
import commandRegistry from "../../services/command_registry";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
|
|
@ -12,34 +12,57 @@ import shortcutService from "../../services/shortcuts";
|
||||||
|
|
||||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||||
|
|
||||||
type Mode = "last-search" | "recent-notes" | "commands";
|
enum Mode {
|
||||||
|
LastSearch,
|
||||||
|
RecentNotes,
|
||||||
|
Commands,
|
||||||
|
}
|
||||||
|
|
||||||
export default function JumpToNoteDialogComponent() {
|
export default function JumpToNoteDialogComponent() {
|
||||||
const [ mode, setMode ] = useState<Mode>();
|
const [ mode, setMode ] = useState<Mode>();
|
||||||
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const autocompleteRef = useRef<HTMLInputElement>(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 [ initialText, setInitialText ] = useState(isCommandMode ? "> " : "");
|
||||||
const actualText = useRef<string>(initialText);
|
const actualText = useRef<string>(initialText);
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
|
|
||||||
async function openDialog(commandMode: boolean) {
|
async function openDialog(requestedMode: Mode) {
|
||||||
let newMode: Mode;
|
let newMode: Mode;
|
||||||
let initialText = "";
|
let initialText = "";
|
||||||
|
|
||||||
if (commandMode) {
|
switch (requestedMode) {
|
||||||
newMode = "commands";
|
case Mode.Commands:
|
||||||
initialText = ">";
|
newMode = Mode.Commands;
|
||||||
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
|
initialText = ">";
|
||||||
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
break;
|
||||||
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
|
|
||||||
// so we'll keep the content.
|
case Mode.LastSearch:
|
||||||
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
|
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
||||||
newMode = "last-search";
|
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
|
||||||
initialText = actualText.current;
|
// so we'll keep the content.
|
||||||
} else {
|
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
|
||||||
newMode = "recent-notes";
|
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) {
|
if (mode !== newMode) {
|
||||||
|
|
@ -51,8 +74,8 @@ export default function JumpToNoteDialogComponent() {
|
||||||
setLastOpenedTs(Date.now());
|
setLastOpenedTs(Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
useTriliumEvent("jumpToNote", () => openDialog(false));
|
useTriliumEvent("jumpToNote", () => openDialog(Mode.RecentNotes));
|
||||||
useTriliumEvent("commandPalette", () => openDialog(true));
|
useTriliumEvent("commandPalette", () => openDialog(Mode.Commands));
|
||||||
|
|
||||||
async function onItemSelected(suggestion?: Suggestion | null) {
|
async function onItemSelected(suggestion?: Suggestion | null) {
|
||||||
if (!suggestion) {
|
if (!suggestion) {
|
||||||
|
|
@ -116,7 +139,7 @@ export default function JumpToNoteDialogComponent() {
|
||||||
container={containerRef}
|
container={containerRef}
|
||||||
text={initialText}
|
text={initialText}
|
||||||
opts={{
|
opts={{
|
||||||
allowCreatingNotes: true,
|
createMode: CreateMode.CreateOnly,
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowJumpToSearchNotes: true,
|
allowJumpToSearchNotes: true,
|
||||||
isCommandPalette: true
|
isCommandPalette: true
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { useEffect, useState } from "preact/hooks";
|
||||||
import note_types from "../../services/note_types";
|
import note_types from "../../services/note_types";
|
||||||
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
|
||||||
import { TreeCommandNames } from "../../menus/tree_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 Badge from "../react/Badge";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ export default function NoteTypeChooserDialogComponent() {
|
||||||
onChange={setParentNote}
|
onChange={setParentNote}
|
||||||
placeholder={t("note_type_chooser.search_placeholder")}
|
placeholder={t("note_type_chooser.search_placeholder")}
|
||||||
opts={{
|
opts={{
|
||||||
allowCreatingNotes: false,
|
createMode: CreateMode.None,
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowJumpToSearchNotes: false,
|
allowJumpToSearchNotes: false,
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import BasicWidget from "../basic_widget.js";
|
||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext from "../../components/app_context.js";
|
||||||
import server from "../../services/server.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 { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
|
||||||
import { formatMarkdown } from "./utils.js";
|
import { formatMarkdown } from "./utils.js";
|
||||||
|
|
@ -163,7 +163,7 @@ export default class LlmChatPanel extends BasicWidget {
|
||||||
const mentionSetup: MentionFeed[] = [
|
const mentionSetup: MentionFeed[] = [
|
||||||
{
|
{
|
||||||
marker: "@",
|
marker: "@",
|
||||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, CreateMode.CreateAndLink),
|
||||||
itemRenderer: (item) => {
|
itemRenderer: (item) => {
|
||||||
const suggestion = item as Suggestion;
|
const suggestion = item as Suggestion;
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default function MobileDetailMenu() {
|
||||||
],
|
],
|
||||||
selectMenuItemHandler: async ({ command }) => {
|
selectMenuItemHandler: async ({ command }) => {
|
||||||
if (command === "insertChildNote") {
|
if (command === "insertChildNote") {
|
||||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
note_create.createNoteIntoPath(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||||
} else if (command === "delete") {
|
} else if (command === "delete") {
|
||||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
if (!notePath) {
|
if (!notePath) {
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||||
// without await as this otherwise causes deadlock through component mutex
|
// without await as this otherwise causes deadlock through component mutex
|
||||||
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
|
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
if (this.noteContext && parentNotePath) {
|
if (this.noteContext && parentNotePath) {
|
||||||
noteCreateService.createNote(parentNotePath, {
|
noteCreateService.createNoteIntoPath(parentNotePath, {
|
||||||
isProtected: note.isProtected,
|
isProtected: note.isProtected,
|
||||||
saveSelection: true,
|
saveSelection: true,
|
||||||
textEditor: await this.noteContext.getTextEditor()
|
textEditor: await this.noteContext.getTextEditor()
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||||
} else if (target.classList.contains("add-note-button")) {
|
} else if (target.classList.contains("add-note-button")) {
|
||||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||||
const parentNotePath = treeService.getNotePath(node);
|
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")) {
|
} else if (target.classList.contains("enter-workspace-button")) {
|
||||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||||
this.triggerCommand("hoistNote", { noteId: node.data.noteId });
|
this.triggerCommand("hoistNote", { noteId: node.data.noteId });
|
||||||
|
|
@ -1843,7 +1843,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||||
const node = this.getActiveNode();
|
const node = this.getActiveNode();
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const notePath = treeService.getNotePath(node);
|
const notePath = treeService.getNotePath(node);
|
||||||
noteCreateService.createNote(notePath, {
|
noteCreateService.createNoteIntoPath(notePath, {
|
||||||
isProtected: node.data.isProtected
|
isProtected: node.data.isProtected
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { t } from "../services/i18n.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import ws from "../services/ws.js";
|
import ws from "../services/ws.js";
|
||||||
import treeService from "../services/tree.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 NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||||
import attributeService from "../services/attributes.js";
|
import attributeService from "../services/attributes.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
|
|
@ -341,7 +341,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
// no need to wait for this
|
// no need to wait for this
|
||||||
noteAutocompleteService.initNoteAutocomplete($input, { allowCreatingNotes: true });
|
noteAutocompleteService.initNoteAutocomplete($input, { createMode: CreateMode.CreateOnly});
|
||||||
|
|
||||||
$input.on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
$input.on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
||||||
this.promotedAttributeChanged(event);
|
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 { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import server from "../../../services/server";
|
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 CKEditor, { CKEditorApi } from "../../react/CKEditor";
|
||||||
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
||||||
import FAttribute from "../../../entities/fattribute";
|
import FAttribute from "../../../entities/fattribute";
|
||||||
|
|
@ -20,6 +20,7 @@ import type { CommandData, FilteredCommandNames } from "../../../components/app_
|
||||||
import { AttributeType } from "@triliumnext/commons";
|
import { AttributeType } from "@triliumnext/commons";
|
||||||
import attributes from "../../../services/attributes";
|
import attributes from "../../../services/attributes";
|
||||||
import note_create from "../../../services/note_create";
|
import note_create from "../../../services/note_create";
|
||||||
|
import { MentionAction } from "@triliumnext/ckeditor5/src/augmentation.js";
|
||||||
|
|
||||||
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ const HELP_TEXT = `
|
||||||
const mentionSetup: MentionFeed[] = [
|
const mentionSetup: MentionFeed[] = [
|
||||||
{
|
{
|
||||||
marker: "@",
|
marker: "@",
|
||||||
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText),
|
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText, CreateMode.CreateAndLink),
|
||||||
itemRenderer: (_item) => {
|
itemRenderer: (_item) => {
|
||||||
const item = _item as Suggestion;
|
const item = _item as Suggestion;
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
|
|
@ -252,17 +253,40 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||||
|
|
||||||
$el.text(title);
|
$el.text(title);
|
||||||
},
|
},
|
||||||
createNoteForReferenceLink: async (title: string) => {
|
createNoteFromCkEditor: async (
|
||||||
let result;
|
title: string,
|
||||||
if (notePath) {
|
parentNotePath: string | undefined,
|
||||||
result = await note_create.createNoteWithTypePrompt(notePath, {
|
action: MentionAction
|
||||||
activate: false,
|
): Promise<string> => {
|
||||||
title: title
|
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 ]));
|
}), [ notePath ]));
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||||
import getTemplates from "./snippets.js";
|
import getTemplates from "./snippets.js";
|
||||||
import { t } from "../../../services/i18n.js";
|
import { t } from "../../../services/i18n.js";
|
||||||
import { getMermaidConfig } from "../../../services/mermaid.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 mimeTypesService from "../../../services/mime_types.js";
|
||||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||||
import { buildToolbarConfig } from "./toolbar.js";
|
import { buildToolbarConfig } from "./toolbar.js";
|
||||||
|
|
@ -181,7 +181,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||||
feeds: [
|
feeds: [
|
||||||
{
|
{
|
||||||
marker: "@",
|
marker: "@",
|
||||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, CreateMode.CreateAndLink),
|
||||||
itemRenderer: (item) => {
|
itemRenderer: (item) => {
|
||||||
const itemElement = document.createElement("button");
|
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 type FNote from "../../entities/fnote.js";
|
||||||
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5";
|
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5";
|
||||||
import { updateTemplateCache } from "./ckeditor/snippets.js";
|
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";
|
export type BoxSize = "small" | "medium" | "full";
|
||||||
|
|
||||||
|
|
@ -491,21 +493,81 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteForReferenceLink(title: string) {
|
async createNoteFromCkEditor (
|
||||||
if (!this.notePath) {
|
title: string,
|
||||||
return;
|
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">) {
|
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 TypeWidget from "./type_widget.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext from "../../components/app_context.js";
|
||||||
import searchService from "../../services/search.js";
|
import searchService from "../../services/search.js";
|
||||||
|
|
@ -77,7 +77,7 @@ export default class EmptyTypeWidget extends TypeWidget {
|
||||||
noteAutocompleteService
|
noteAutocompleteService
|
||||||
.initNoteAutocomplete(this.$autoComplete, {
|
.initNoteAutocomplete(this.$autoComplete, {
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowCreatingNotes: true,
|
createMode: CreateMode.CreateOnly,
|
||||||
allowJumpToSearchNotes: true,
|
allowJumpToSearchNotes: true,
|
||||||
container: this.$results[0]
|
container: this.$results[0]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
import "ckeditor5";
|
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 {
|
declare global {
|
||||||
interface Component {
|
interface Component {
|
||||||
|
|
@ -7,7 +15,8 @@ declare global {
|
||||||
|
|
||||||
interface EditorComponent extends Component {
|
interface EditorComponent extends Component {
|
||||||
loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string): Promise<void>;
|
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;
|
loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "ckeditor5";
|
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):
|
* 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 {
|
export default class MentionCustomization extends Plugin {
|
||||||
|
|
||||||
static get requires() {
|
static get requires() {
|
||||||
return [ Mention ];
|
return [ Mention ];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get pluginName() {
|
public static get pluginName() {
|
||||||
return "MentionCustomization" as const;
|
return "MentionCustomization" as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,20 +26,21 @@ export default class MentionCustomization extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MentionOpts {
|
interface MentionOpts {
|
||||||
mention: string | {
|
mention: string | {
|
||||||
id: string;
|
id: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
marker: string;
|
marker: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
range?: ModelRange;
|
range?: ModelRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MentionAttribute {
|
interface MentionAttribute {
|
||||||
id: string;
|
id: string;
|
||||||
action?: "create-note";
|
action?: MentionAction;
|
||||||
noteTitle: string;
|
noteTitle: string;
|
||||||
notePath: string;
|
notePath: string;
|
||||||
|
parentNoteId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomMentionCommand extends Command {
|
class CustomMentionCommand extends Command {
|
||||||
|
|
@ -56,14 +58,27 @@ class CustomMentionCommand extends Command {
|
||||||
model.insertContent( writer.createText( mention.id, {} ), range );
|
model.insertContent( writer.createText( mention.id, {} ), range );
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (mention.action === 'create-note') {
|
else if (
|
||||||
const editorEl = this.editor.editing.view.getDomRoot();
|
mention.action === MentionAction.CreateNoteIntoInbox ||
|
||||||
const component = glob.getComponentByEl<EditorComponent>(editorEl);
|
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 => {
|
// use parentNoteId as fallback when notePath is missing
|
||||||
this.insertReference(range, notePath);
|
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 {
|
else {
|
||||||
this.insertReference(range, mention.notePath);
|
this.insertReference(range, mention.notePath);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue