mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
Compare commits
70 Commits
7c9bf4a48e
...
b531257fd2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b531257fd2 | ||
|
|
32c16021c4 | ||
|
|
7713c1173a | ||
|
|
8018f400c3 | ||
|
|
79c8293881 | ||
|
|
db5652623b | ||
|
|
0f7a48b323 | ||
|
|
415d2826c6 | ||
|
|
7787e7085e | ||
|
|
4ab8417168 | ||
|
|
8ee59e9daa | ||
|
|
4e5c26371e | ||
|
|
436146d829 | ||
|
|
315fcecf57 | ||
|
|
0a57e6e154 | ||
|
|
bdcb84a394 | ||
|
|
213c36ba84 | ||
|
|
995e765276 | ||
|
|
8e81c38c14 | ||
|
|
7378fa4cbd | ||
|
|
af8a5ff0c9 | ||
|
|
eca5a4a072 | ||
|
|
71b3ad5027 | ||
|
|
7864168adc | ||
|
|
f8090d9217 | ||
|
|
09aa22c74b | ||
|
|
8fc8f97879 | ||
|
|
547cdff510 | ||
|
|
cdd08d6971 | ||
|
|
b3cf9c8f2d | ||
|
|
feefa389b4 | ||
|
|
f65be4f368 | ||
|
|
77a014109e | ||
|
|
d3db48c99b | ||
|
|
7e4833e893 | ||
|
|
470d7eee31 | ||
|
|
aada631c0f | ||
|
|
bc4186d216 | ||
|
|
c2a27eff2c | ||
|
|
ca24408a13 | ||
|
|
b9e19e524a | ||
|
|
09c8a778f5 | ||
|
|
3438f1103d | ||
|
|
82a3be06d1 | ||
|
|
f0dead5390 | ||
|
|
b0fdb9fef2 | ||
|
|
71009bddc7 | ||
|
|
66e499a2e1 | ||
|
|
a5ef5eee2f | ||
|
|
bcb29d22f5 | ||
|
|
6ad2b49ab3 | ||
|
|
656e7c069d | ||
|
|
00aa470bf2 | ||
|
|
c6ed0b43fc | ||
|
|
3d8a4d2553 | ||
|
|
42ab0eb804 | ||
|
|
d80d06a9b8 | ||
|
|
3c39026739 | ||
|
|
72c17b22df | ||
|
|
dd1aa23cb6 | ||
|
|
ecdf243e63 | ||
|
|
1e15585a24 | ||
|
|
9d40c0cb26 | ||
|
|
e41e24b044 | ||
|
|
a15d661fd7 | ||
|
|
cb5954e8c7 | ||
|
|
2b55db05e1 | ||
|
|
74bf93059c | ||
|
|
384d8c9c37 | ||
|
|
1bb6149dbe |
@ -25,7 +25,7 @@ import TouchBarComponent from "./touch_bar.js";
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { StartupChecks } from "./startup_checks.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import type { CreateNoteOpts, CreateNoteWithLinkOpts } from "../services/note_create.js";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
@ -359,8 +359,7 @@ export type CommandMappings = {
|
||||
|
||||
// Table view
|
||||
addNewRow: CommandData & {
|
||||
customOpts: CreateNoteOpts;
|
||||
parentNotePath?: string;
|
||||
customOpts?: CreateNoteWithLinkOpts;
|
||||
};
|
||||
addNewTableColumn: CommandData & {
|
||||
columnToEdit?: ColumnComponent;
|
||||
|
||||
@ -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.js";
|
||||
|
||||
export default class Entrypoints extends Component {
|
||||
constructor() {
|
||||
@ -24,23 +25,9 @@ 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.createNote(
|
||||
{ target: "default" }
|
||||
);
|
||||
}
|
||||
|
||||
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
|
||||
|
||||
@ -48,10 +48,15 @@ export default class MainTreeExecutors extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
await noteCreateService.createNote(activeNoteContext.notePath, {
|
||||
isProtected: activeNoteContext.note.isProtected,
|
||||
saveSelection: false
|
||||
});
|
||||
await noteCreateService.createNote(
|
||||
{
|
||||
target: "into",
|
||||
parentNoteLink: activeNoteContext.notePath,
|
||||
isProtected: activeNoteContext.note.isProtected,
|
||||
saveSelection: false,
|
||||
promptForType: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async createNoteAfterCommand() {
|
||||
@ -72,11 +77,14 @@ export default class MainTreeExecutors extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
await noteCreateService.createNote(parentNotePath, {
|
||||
target: "after",
|
||||
targetBranchId: node.data.branchId,
|
||||
isProtected: isProtected,
|
||||
saveSelection: false
|
||||
});
|
||||
await noteCreateService.createNote(
|
||||
{
|
||||
target: "after",
|
||||
parentNoteLink: parentNotePath,
|
||||
targetBranchId: node.data.branchId,
|
||||
isProtected: isProtected,
|
||||
saveSelection: false
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ export default class RootCommandExecutor extends Component {
|
||||
}
|
||||
|
||||
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
||||
const noteId = treeService.getNoteIdFromUrl(notePath);
|
||||
const noteId = treeService.getNoteIdFromLink(notePath);
|
||||
|
||||
this.searchNotesCommand({ ancestorNoteId: noteId });
|
||||
}
|
||||
@ -240,14 +240,18 @@ 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, {
|
||||
title: "New AI Chat",
|
||||
type: "aiChat",
|
||||
content: JSON.stringify({
|
||||
messages: [],
|
||||
title: "New AI Chat"
|
||||
})
|
||||
});
|
||||
const result = await noteCreateService.createNote(
|
||||
{
|
||||
parentNoteLink: rootNoteId,
|
||||
target: "into",
|
||||
title: "New AI Chat",
|
||||
type: "aiChat",
|
||||
content: JSON.stringify({
|
||||
messages: [],
|
||||
title: "New AI Chat"
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.note) {
|
||||
toastService.showError("Failed to create AI Chat note");
|
||||
|
||||
@ -74,10 +74,10 @@ export default class TabManager extends Component {
|
||||
|
||||
// preload all notes at once
|
||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
||||
[treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true);
|
||||
[treeService.getNoteIdFromLink(tab.notePath), tab.hoistedNoteId])], true);
|
||||
|
||||
const filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => {
|
||||
const noteId = treeService.getNoteIdFromUrl(openTab.notePath);
|
||||
const noteId = treeService.getNoteIdFromLink(openTab.notePath);
|
||||
if (!noteId || !(noteId in froca.notes)) {
|
||||
// note doesn't exist so don't try to open tab for it
|
||||
return false;
|
||||
|
||||
@ -283,21 +283,31 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const parentNotePath = treeService.getNotePath(this.node.getParent());
|
||||
const isProtected = treeService.getParentProtectedStatus(this.node);
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
target: "after",
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type: type,
|
||||
isProtected: isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
|
||||
noteCreateService.createNote(
|
||||
{
|
||||
target: "after",
|
||||
parentNoteLink: parentNotePath,
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type: type,
|
||||
isProtected: isProtected,
|
||||
templateNoteId: templateNoteId,
|
||||
promptForType: false,
|
||||
}
|
||||
);
|
||||
} else if (command === "insertChildNote") {
|
||||
const parentNotePath = treeService.getNotePath(this.node);
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
type: type,
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
});
|
||||
noteCreateService.createNote(
|
||||
{
|
||||
target: "into",
|
||||
parentNoteLink: parentNotePath,
|
||||
type: type,
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId: templateNoteId,
|
||||
promptForType: false,
|
||||
}
|
||||
);
|
||||
} else if (command === "openNoteInSplit") {
|
||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||
|
||||
@ -50,7 +50,7 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
|
||||
const hoistedNoteId = noteContext.hoistedNoteId;
|
||||
|
||||
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) {
|
||||
const noteId = treeService.getNoteIdFromUrl(resolvedNotePath);
|
||||
const noteId = treeService.getNoteIdFromLink(resolvedNotePath);
|
||||
if (!noteId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -261,7 +261,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
|
||||
return {
|
||||
notePath,
|
||||
noteId: treeService.getNoteIdFromUrl(notePath),
|
||||
noteId: treeService.getNoteIdFromLink(notePath),
|
||||
ntxId,
|
||||
hoistedNoteId,
|
||||
viewScope,
|
||||
|
||||
@ -5,6 +5,24 @@ import froca from "./froca.js";
|
||||
import { t } from "./i18n.js";
|
||||
import commandRegistry from "./command_registry.js";
|
||||
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
|
||||
import { CreateNoteAction } from "@triliumnext/commons"
|
||||
import FNote from "../entities/fnote.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 +41,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 CreateNoteAction 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
|
||||
CreateNote = CreateNoteAction.CreateNote,
|
||||
CreateChildNote = CreateNoteAction.CreateChildNote,
|
||||
CreateAndLinkNote = CreateNoteAction.CreateAndLinkNote,
|
||||
CreateAndLinkChildNote = CreateNoteAction.CreateAndLinkChildNote,
|
||||
|
||||
SearchNotes = "search-notes",
|
||||
ExternalLink = "external-link",
|
||||
Command = "command",
|
||||
}
|
||||
|
||||
export enum SuggestionMode {
|
||||
SuggestNothing = "nothing",
|
||||
SuggestCreateOnly = "create-only",
|
||||
SuggestCreateAndLink = "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 +86,7 @@ export interface Suggestion {
|
||||
export interface Options {
|
||||
container?: HTMLElement | null;
|
||||
fastSearch?: boolean;
|
||||
allowCreatingNotes?: boolean;
|
||||
suggestionMode?: SuggestionMode;
|
||||
allowJumpToSearchNotes?: boolean;
|
||||
allowExternalLinks?: boolean;
|
||||
/** If set, hides the right-side button corresponding to go to selected note. */
|
||||
@ -54,110 +97,160 @@ export interface Options {
|
||||
isCommandPalette?: boolean;
|
||||
}
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
||||
return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
|
||||
async function autocompleteSourceForCKEditor(
|
||||
queryText: string,
|
||||
suggestionMode: SuggestionMode
|
||||
): 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
|
||||
suggestionMode,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 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.suggestionMode) {
|
||||
case SuggestionMode.SuggestCreateOnly: {
|
||||
results = [
|
||||
{
|
||||
action: SuggestionAction.CreateNote,
|
||||
noteTitle: trimmedTerm,
|
||||
parentNoteId: "inbox",
|
||||
highlightedNotePathTitle: t("note_autocomplete.create-note", { term: trimmedTerm }),
|
||||
},
|
||||
{
|
||||
action: SuggestionAction.CreateChildNote,
|
||||
noteTitle: trimmedTerm,
|
||||
parentNoteId: activeNoteId || "root",
|
||||
highlightedNotePathTitle: t("note_autocomplete.create-child-note", { term: trimmedTerm }),
|
||||
},
|
||||
...results,
|
||||
];
|
||||
break;
|
||||
}
|
||||
]);
|
||||
|
||||
case SuggestionMode.SuggestCreateAndLink: {
|
||||
results = [
|
||||
{
|
||||
action: SuggestionAction.CreateAndLinkNote,
|
||||
noteTitle: trimmedTerm,
|
||||
parentNoteId: "inbox",
|
||||
highlightedNotePathTitle: t("note_autocomplete.create-and-link-note", { term: trimmedTerm }),
|
||||
},
|
||||
{
|
||||
action: SuggestionAction.CreateAndLinkChildNote,
|
||||
noteTitle: trimmedTerm,
|
||||
parentNoteId: activeNoteId || "root",
|
||||
highlightedNotePathTitle: t("note_autocomplete.create-and-link-child-note", { 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 +291,85 @@ 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.CreateAndLinkNote:
|
||||
case SuggestionAction.CreateNote:
|
||||
return "bx bx-plus";
|
||||
case SuggestionAction.CreateAndLinkChildNote:
|
||||
case SuggestionAction.CreateChildNote:
|
||||
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 mapSuggestionToCreateNoteAction(
|
||||
action: SuggestionAction
|
||||
): CreateNoteAction | null {
|
||||
switch (action) {
|
||||
case SuggestionAction.CreateNote:
|
||||
return CreateNoteAction.CreateNote;
|
||||
|
||||
case SuggestionAction.CreateAndLinkNote:
|
||||
return CreateNoteAction.CreateAndLinkNote;
|
||||
|
||||
case SuggestionAction.CreateChildNote:
|
||||
return CreateNoteAction.CreateChildNote;
|
||||
|
||||
case SuggestionAction.CreateAndLinkChildNote:
|
||||
return CreateNoteAction.CreateAndLinkChildNote;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 +455,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 +477,85 @@ 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") {
|
||||
async function doCommand() {
|
||||
$el.autocomplete("close");
|
||||
$el.trigger("autocomplete:commandselected", [suggestion]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (suggestion.action === "external-link") {
|
||||
async function doExternalLink() {
|
||||
$el.setSelectedNotePath(null);
|
||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||
|
||||
$el.autocomplete("val", suggestion.externalLink);
|
||||
$el.autocomplete("close");
|
||||
$el.trigger("autocomplete:externallinkselected", [suggestion]);
|
||||
}
|
||||
|
||||
async function resolveSuggestionNotePathUnderCurrentHoist(note: FNote) {
|
||||
const hoisted = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
suggestion.notePath = note.getBestNotePathString(hoisted);
|
||||
}
|
||||
|
||||
async function doSearchNotes() {
|
||||
const searchString = suggestion.noteTitle;
|
||||
appContext.triggerCommand("searchNotes", { searchString });
|
||||
}
|
||||
|
||||
async function selectNoteFromAutocomplete(suggestion: Suggestion) {
|
||||
$el.setSelectedNotePath(suggestion.notePath);
|
||||
$el.setSelectedExternalLink(null);
|
||||
|
||||
$el.autocomplete("val", suggestion.noteTitle);
|
||||
|
||||
$el.autocomplete("close");
|
||||
|
||||
$el.trigger("autocomplete:externallinkselected", [suggestion]);
|
||||
|
||||
return;
|
||||
$el.trigger("autocomplete:noteselected", [suggestion]);
|
||||
}
|
||||
|
||||
if (suggestion.action === "create-note") {
|
||||
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
|
||||
if (!success) {
|
||||
switch (suggestion.action) {
|
||||
case SuggestionAction.Command:
|
||||
await doCommand();
|
||||
return;
|
||||
|
||||
case SuggestionAction.ExternalLink:
|
||||
await doExternalLink();
|
||||
break;
|
||||
|
||||
case SuggestionAction.CreateNote:
|
||||
case SuggestionAction.CreateAndLinkNote:
|
||||
case SuggestionAction.CreateChildNote:
|
||||
case SuggestionAction.CreateAndLinkChildNote: {
|
||||
const createNoteAction = mapSuggestionToCreateNoteAction(
|
||||
suggestion.action
|
||||
)!;
|
||||
const { note } = await noteCreateService.createNoteFromAction(
|
||||
createNoteAction,
|
||||
true,
|
||||
suggestion.noteTitle,
|
||||
suggestion.parentNoteId,
|
||||
);
|
||||
|
||||
if (!note) break;
|
||||
|
||||
await resolveSuggestionNotePathUnderCurrentHoist(note);
|
||||
await selectNoteFromAutocomplete(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.SearchNotes:
|
||||
await doSearchNotes();
|
||||
break;
|
||||
|
||||
default:
|
||||
await selectNoteFromAutocomplete(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,8 +10,63 @@ 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 { CreateNoteAction } from "@triliumnext/commons";
|
||||
|
||||
export interface CreateNoteOpts {
|
||||
/**
|
||||
* Defines the type hierarchy and rules for valid argument combinations
|
||||
* accepted by `note_create`.
|
||||
*
|
||||
* ## Overview
|
||||
* Each variant extends `CreateNoteOpts` and enforces specific constraints to
|
||||
* ensure only valid note creation options are allowed at compile time.
|
||||
*
|
||||
* ## Type Safety
|
||||
* The `PromptingRule` ensures that `promptForType` and `type` stay mutually
|
||||
* exclusive (if prompting, `type` is undefined).
|
||||
*
|
||||
* The type system prevents invalid argument mixes by design — successful type
|
||||
* checks guarantee a valid state, following Curry–Howard correspondence
|
||||
* principles (types as proofs).
|
||||
*
|
||||
* ## Maintenance
|
||||
* If adding or modifying `Opts`, ensure:
|
||||
* - All valid combinations are represented (avoid *false negatives*).
|
||||
* - No invalid ones slip through (avoid *false positives*).
|
||||
*
|
||||
* Hierarchy (general → specific):
|
||||
* - CreateNoteOpts
|
||||
* - CreateNoteWithUrlOpts
|
||||
* - CreateNoteIntoDefaultOpts
|
||||
*/
|
||||
|
||||
/** enforces a truth rule:
|
||||
* - If `promptForType` is true → `type` must be undefined.
|
||||
* - If `promptForType` is false → `type` must be defined.
|
||||
*/
|
||||
type PromptingRule = {
|
||||
promptForType: true;
|
||||
type?: never;
|
||||
} | {
|
||||
promptForType?: false;
|
||||
/**
|
||||
* The note type (e.g. "text", "code", "image", "mermaid", etc.).
|
||||
*
|
||||
* If omitted, the server will automatically default to `"text"`.
|
||||
* TypeScript still enforces explicit typing unless `promptForType` is true,
|
||||
* to encourage clarity at the call site.
|
||||
*/
|
||||
type?: string;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Base type for all note creation options (domain hypernym).
|
||||
* All specific note option types extend from this.
|
||||
*
|
||||
* Combine with `&` to ensure valid logical combinations.
|
||||
*/
|
||||
type CreateNoteBase = {
|
||||
isProtected?: boolean;
|
||||
saveSelection?: boolean;
|
||||
title?: string | null;
|
||||
@ -21,10 +76,34 @@ export interface CreateNoteOpts {
|
||||
templateNoteId?: string;
|
||||
activate?: boolean;
|
||||
focus?: "title" | "content";
|
||||
target?: string;
|
||||
targetBranchId?: string;
|
||||
textEditor?: CKTextEditor;
|
||||
}
|
||||
} & PromptingRule;
|
||||
|
||||
/*
|
||||
* Defines options for creating a note at a specific path.
|
||||
* Serves as a base for "into", "before", and "after" variants,
|
||||
* sharing common URL-related fields.
|
||||
*/
|
||||
export type CreateNoteWithLinkOpts =
|
||||
| (CreateNoteBase & {
|
||||
target: "into";
|
||||
parentNoteLink?: string;
|
||||
// No branch ID needed for "into"
|
||||
})
|
||||
| (CreateNoteBase & {
|
||||
target: "before" | "after";
|
||||
// Either an Url or a Path
|
||||
parentNoteLink?: string;
|
||||
// Required for "before"/"after"
|
||||
targetBranchId: string;
|
||||
});
|
||||
|
||||
export type CreateNoteIntoDefaultOpts = CreateNoteBase & {
|
||||
target: "default";
|
||||
parentNoteLink?: never;
|
||||
};
|
||||
|
||||
export type CreateNoteOpts = CreateNoteWithLinkOpts | CreateNoteIntoDefaultOpts;
|
||||
|
||||
interface Response {
|
||||
// TODO: Deduplicate with server once we have client/server architecture.
|
||||
@ -37,7 +116,141 @@ interface DuplicateResponse {
|
||||
note: FNote;
|
||||
}
|
||||
|
||||
async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) {
|
||||
// The low level note creation
|
||||
async function createNote(
|
||||
options: CreateNoteOpts
|
||||
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
|
||||
|
||||
let resolvedOptions = { ...options };
|
||||
|
||||
// handle prompts centrally to write once fix for all
|
||||
if (options.promptForType) {
|
||||
const maybeResolvedOptions = await promptForType(options);
|
||||
if (!maybeResolvedOptions) {
|
||||
return { note: null, branch: undefined };
|
||||
}
|
||||
|
||||
resolvedOptions = maybeResolvedOptions;
|
||||
}
|
||||
|
||||
|
||||
switch(resolvedOptions.target) {
|
||||
case "default":
|
||||
return createNoteIntoDefaultLocation(resolvedOptions);
|
||||
case "into":
|
||||
case "before":
|
||||
case "after":
|
||||
return createNoteWithLink(resolvedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
// A wrapper to standardize note creation
|
||||
async function createNoteFromAction(
|
||||
action: CreateNoteAction,
|
||||
promptForType: boolean,
|
||||
title: string | undefined,
|
||||
parentNoteLink: string | undefined,
|
||||
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
|
||||
switch (action) {
|
||||
case CreateNoteAction.CreateNote: {
|
||||
const resp = await createNote(
|
||||
{
|
||||
target: "default",
|
||||
title: title,
|
||||
activate: true,
|
||||
promptForType,
|
||||
}
|
||||
);
|
||||
return resp;
|
||||
}
|
||||
case CreateNoteAction.CreateAndLinkNote: {
|
||||
const resp = await createNote(
|
||||
{
|
||||
target: "default",
|
||||
title,
|
||||
activate: false,
|
||||
promptForType,
|
||||
}
|
||||
);
|
||||
return resp;
|
||||
}
|
||||
case CreateNoteAction.CreateChildNote: {
|
||||
if (!parentNoteLink) {
|
||||
console.warn("createNoteFromAction: Missing parentNoteLink");
|
||||
return { note: null, branch: undefined };
|
||||
}
|
||||
|
||||
const resp = await createNote(
|
||||
{
|
||||
target: "into",
|
||||
parentNoteLink,
|
||||
title,
|
||||
activate: true,
|
||||
promptForType,
|
||||
},
|
||||
);
|
||||
return resp
|
||||
}
|
||||
case CreateNoteAction.CreateAndLinkChildNote: {
|
||||
if (!parentNoteLink) {
|
||||
console.warn("createNoteFromAction: Missing parentNoteLink");
|
||||
return { note: null, branch: undefined };
|
||||
}
|
||||
const resp = await createNote(
|
||||
{
|
||||
target: "into",
|
||||
parentNoteLink: parentNoteLink,
|
||||
title,
|
||||
activate: false,
|
||||
promptForType,
|
||||
},
|
||||
)
|
||||
return resp;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn("Unknown CreateNoteAction:", action);
|
||||
return { note: null, branch: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForType(
|
||||
options: CreateNoteOpts
|
||||
) : Promise<CreateNoteOpts | null> {
|
||||
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
|
||||
|
||||
if (!success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let resolvedOptions: CreateNoteOpts = {
|
||||
...options,
|
||||
promptForType: false,
|
||||
type: noteType,
|
||||
templateNoteId,
|
||||
};
|
||||
|
||||
if (notePath) {
|
||||
resolvedOptions = {
|
||||
...resolvedOptions,
|
||||
target: "into",
|
||||
parentNoteLink: notePath,
|
||||
};
|
||||
}
|
||||
|
||||
return resolvedOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note under a specified parent note path.
|
||||
*
|
||||
* @param target - Mirrors the `createNote` API in apps/server/src/routes/api/notes.ts.
|
||||
* @param options - Note creation options
|
||||
* @returns A promise resolving with the created note and its branch.
|
||||
*/
|
||||
async function createNoteWithLink(
|
||||
options: CreateNoteWithLinkOpts
|
||||
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
|
||||
options = Object.assign(
|
||||
{
|
||||
activate: true,
|
||||
@ -61,7 +274,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
||||
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
|
||||
}
|
||||
|
||||
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
|
||||
const parentNoteLink = options.parentNoteLink;
|
||||
const parentNoteId = treeService.getNoteIdFromLink(parentNoteLink);
|
||||
|
||||
if (options.type === "mermaid" && !options.content && !options.templateNoteId) {
|
||||
options.content = `graph TD;
|
||||
@ -71,7 +285,12 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
||||
C-->D;`;
|
||||
}
|
||||
|
||||
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
|
||||
const query =
|
||||
options.target === "into"
|
||||
? `target=${options.target}`
|
||||
: `target=${options.target}&targetBranchId=${options.targetBranchId}`;
|
||||
|
||||
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?${query}`, {
|
||||
title: options.title,
|
||||
content: options.content || "",
|
||||
isProtected: options.isProtected,
|
||||
@ -89,7 +308,7 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
||||
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
if (activeNoteContext && options.activate) {
|
||||
await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
|
||||
await activeNoteContext.setNote(`${parentNoteId}/${note.noteId}`);
|
||||
|
||||
if (options.focus === "title") {
|
||||
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
|
||||
@ -107,25 +326,46 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new note inside the user's Inbox.
|
||||
*
|
||||
* @param {CreateNoteIntoDefaultOpts} [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 createNoteIntoDefaultLocation(
|
||||
options: CreateNoteIntoDefaultOpts
|
||||
): 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 createNoteWithLink(
|
||||
{
|
||||
...options,
|
||||
target: "into",
|
||||
parentNoteLink: inboxNote.getBestNotePathString(),
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function chooseNoteType() {
|
||||
return new Promise<ChooseNoteTypeResponse>((res) => {
|
||||
appContext.triggerCommand("chooseNoteType", { callback: res });
|
||||
});
|
||||
}
|
||||
|
||||
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
|
||||
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.type = noteType;
|
||||
options.templateNoteId = templateNoteId;
|
||||
|
||||
return await createNote(notePath || parentNotePath, options);
|
||||
}
|
||||
|
||||
/* If the first element is heading, parse it out and use it as a new heading. */
|
||||
function parseSelectedHtml(selectedHtml: string) {
|
||||
const dom = $.parseHTML(selectedHtml);
|
||||
@ -146,7 +386,7 @@ function parseSelectedHtml(selectedHtml: string) {
|
||||
}
|
||||
|
||||
async function duplicateSubtree(noteId: string, parentNotePath: string) {
|
||||
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
|
||||
const parentNoteId = treeService.getNoteIdFromLink(parentNotePath);
|
||||
const { note } = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
@ -159,7 +399,6 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) {
|
||||
|
||||
export default {
|
||||
createNote,
|
||||
createNoteWithTypePrompt,
|
||||
createNoteFromAction,
|
||||
duplicateSubtree,
|
||||
chooseNoteType
|
||||
};
|
||||
|
||||
@ -92,7 +92,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
||||
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
|
||||
return effectivePathSegments;
|
||||
} else {
|
||||
const noteId = getNoteIdFromUrl(notePath);
|
||||
const noteId = getNoteIdFromLink(notePath);
|
||||
if (!noteId) {
|
||||
throw new Error(`Unable to find note with ID: ${noteId}.`);
|
||||
}
|
||||
@ -129,7 +129,7 @@ function getParentProtectedStatus(node: Fancytree.FancytreeNode) {
|
||||
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
|
||||
}
|
||||
|
||||
function getNoteIdFromUrl(urlOrNotePath: string | null | undefined) {
|
||||
function getNoteIdFromLink(urlOrNotePath: string | null | undefined) {
|
||||
if (!urlOrNotePath) {
|
||||
return null;
|
||||
}
|
||||
@ -306,7 +306,7 @@ export default {
|
||||
getParentProtectedStatus,
|
||||
getNotePath,
|
||||
getNotePathTitleComponents,
|
||||
getNoteIdFromUrl,
|
||||
getNoteIdFromLink,
|
||||
getNoteIdAndParentIdFromUrl,
|
||||
getBranchIdFromUrl,
|
||||
getNoteTitle,
|
||||
|
||||
@ -27,6 +27,9 @@
|
||||
--bs-body-bg: var(--main-background-color) !important;
|
||||
--ck-mention-list-max-height: 500px;
|
||||
--tn-modal-max-height: 90vh;
|
||||
|
||||
--tree-item-light-theme-max-color-lightness: 50;
|
||||
--tree-item-dark-theme-min-color-lightness: 75;
|
||||
}
|
||||
|
||||
body#trilium-app.motion-disabled *,
|
||||
@ -2579,4 +2582,12 @@ iframe.print-iframe {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Calendar collection */
|
||||
|
||||
.calendar-view a.fc-timegrid-event,
|
||||
.calendar-view a.fc-daygrid-event {
|
||||
/* Workaround: set font weight only if the theme-next is not active */
|
||||
font-weight: var(--root-background, 800);
|
||||
}
|
||||
@ -76,6 +76,9 @@
|
||||
|
||||
--mermaid-theme: dark;
|
||||
--native-titlebar-background: #00000000;
|
||||
|
||||
--calendar-coll-event-background-saturation: 30%;
|
||||
--calendar-coll-event-background-lightness: 30%;
|
||||
}
|
||||
|
||||
body ::-webkit-calendar-picker-indicator {
|
||||
|
||||
@ -80,6 +80,9 @@ html {
|
||||
|
||||
--mermaid-theme: default;
|
||||
--native-titlebar-background: #ffffff00;
|
||||
|
||||
--calendar-coll-event-background-lightness: 95%;
|
||||
--calendar-coll-event-background-saturation: 80%;
|
||||
}
|
||||
|
||||
#left-pane .fancytree-node.tinted {
|
||||
|
||||
@ -271,11 +271,12 @@
|
||||
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
|
||||
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
|
||||
|
||||
--calendar-coll-event-background-saturation: 12%;
|
||||
--calendar-coll-event-background-lightness: 21%;
|
||||
--calendar-coll-event-background-saturation: 25%;
|
||||
--calendar-coll-event-background-lightness: 20%;
|
||||
--calendar-coll-event-background-color: #3c3c3c;
|
||||
--calendar-coll-event-text-color: white;
|
||||
--calendar-cell-event-hover-filter: brightness(1.25);
|
||||
--calendar-coll-today-background-color: #ffffff08;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -274,6 +274,7 @@
|
||||
--calendar-coll-event-background-color: #eaeaea;
|
||||
--calendar-coll-event-text-color: black;
|
||||
--calendar-cell-event-hover-filter: brightness(.95) saturate(1.25);
|
||||
--calendar-coll-today-background-color: #00000006;
|
||||
}
|
||||
|
||||
#left-pane .fancytree-node.tinted {
|
||||
|
||||
@ -1897,7 +1897,10 @@
|
||||
},
|
||||
"note_autocomplete": {
|
||||
"search-for": "Search for \"{{term}}\"",
|
||||
"create-note": "Create and link child note \"{{term}}\"",
|
||||
"create-child-note": "Create child note \"{{term}}\"",
|
||||
"create-note": "Create note \"{{term}}\"",
|
||||
"create-and-link-child-note": "Create and link child note \"{{term}}\"",
|
||||
"create-and-link-note": "Create 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, { SuggestionMode } 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, { suggestionMode: SuggestionMode.SuggestCreateAndLink }).on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
||||
if (!suggestion.notePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import branches from "../../../services/branches";
|
||||
import { executeBulkActions } from "../../../services/bulk_action";
|
||||
import froca from "../../../services/froca";
|
||||
import { t } from "../../../services/i18n";
|
||||
import note_create from "../../../services/note_create";
|
||||
import note_create from "../../../services/note_create.js";
|
||||
import server from "../../../services/server";
|
||||
import { ColumnMap } from "./data";
|
||||
|
||||
@ -39,9 +39,11 @@ 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.createNote({
|
||||
target: "into",
|
||||
parentNoteLink: parentNotePath,
|
||||
activate: false,
|
||||
title
|
||||
title,
|
||||
});
|
||||
|
||||
if (newNote && newBranch) {
|
||||
@ -139,13 +141,17 @@ export default class BoardApi {
|
||||
async insertRowAtPosition(
|
||||
column: string,
|
||||
relativeToBranchId: string,
|
||||
direction: "before" | "after") {
|
||||
const { note, branch } = await note_create.createNote(this.parentNote.noteId, {
|
||||
activate: false,
|
||||
targetBranchId: relativeToBranchId,
|
||||
target: direction,
|
||||
title: t("board_view.new-item")
|
||||
});
|
||||
direction: "before" | "after"
|
||||
) {
|
||||
const { note, branch } = await note_create.createNote(
|
||||
{
|
||||
target: direction,
|
||||
parentNoteLink: this.parentNote.noteId,
|
||||
activate: false,
|
||||
targetBranchId: relativeToBranchId,
|
||||
title: t("board_view.new-item"),
|
||||
}
|
||||
);
|
||||
|
||||
if (!note || !branch) {
|
||||
throw new Error("Failed to create note");
|
||||
|
||||
@ -57,12 +57,18 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
|
||||
{
|
||||
title: t("board_view.insert-above"),
|
||||
uiIcon: "bx bx-list-plus",
|
||||
handler: () => api.insertRowAtPosition(column, branchId, "before")
|
||||
handler: () => api.insertRowAtPosition(
|
||||
column,
|
||||
branchId,
|
||||
"before")
|
||||
},
|
||||
{
|
||||
title: t("board_view.insert-below"),
|
||||
uiIcon: "bx bx-empty",
|
||||
handler: () => api.insertRowAtPosition(column, branchId, "after")
|
||||
handler: () => api.insertRowAtPosition(
|
||||
column,
|
||||
branchId,
|
||||
"after")
|
||||
},
|
||||
{ kind: "separator" },
|
||||
{
|
||||
|
||||
@ -15,6 +15,7 @@ import FormTextArea from "../../react/FormTextArea";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import NoteAutocomplete from "../../react/NoteAutocomplete";
|
||||
import toast from "../../../services/toast";
|
||||
import { SuggestionMode } from "../../../services/note_autocomplete";
|
||||
|
||||
export interface BoardViewData {
|
||||
columns?: BoardColumnData[];
|
||||
@ -309,7 +310,7 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
|
||||
noteId={currentValue ?? ""}
|
||||
opts={{
|
||||
hideAllButtons: true,
|
||||
allowCreatingNotes: true
|
||||
suggestionMode: SuggestionMode.SuggestCreateAndLink
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
|
||||
@ -81,7 +81,6 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg)
|
||||
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) {
|
||||
const customTitleAttributeName = note.getLabelValue("calendar:title");
|
||||
const titles = await parseCustomTitle(customTitleAttributeName, note);
|
||||
const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color");
|
||||
const colorClass = note.getColorClass();
|
||||
const events: EventInput[] = [];
|
||||
|
||||
@ -110,7 +109,6 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
|
||||
start: startDate,
|
||||
url: `#${note.noteId}?popup`,
|
||||
noteId: note.noteId,
|
||||
color: color ?? undefined,
|
||||
iconClass: note.getLabelValue("iconClass"),
|
||||
promotedAttributes: displayedAttributesData,
|
||||
className: clsx({archived: isArchived}, colorClass)
|
||||
|
||||
@ -1,8 +1,19 @@
|
||||
:root {
|
||||
/* Default values to be overridden by themes */
|
||||
--calendar-coll-event-background-lightness: 95%;
|
||||
--calendar-coll-event-background-saturation: 80%;
|
||||
--calendar-coll-event-background-color: var(--accented-background-color);
|
||||
--calendar-coll-event-text-color: var(--primary-button-text-color);
|
||||
--calendar-cell-event-hover-filter: none;
|
||||
--calendar-coll-today-background-color: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.calendar-view {
|
||||
--fc-event-border-color: var(--calendar-coll-event-text-color);
|
||||
--fc-event-bg-color: var(--calendar-coll-event-background-color);
|
||||
--fc-event-text-color: var(--calendar-coll-event-text-color);
|
||||
--fc-event-selected-overlay-color: transparent;
|
||||
--fc-today-bg-color: var(--calendar-coll-today-background-color);
|
||||
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@ -12,8 +23,9 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.calendar-view a {
|
||||
color: unset;
|
||||
.calendar-view a,
|
||||
:root .calendar-view a.fc-daygrid-event:hover {
|
||||
color: var(--fc-event-text-color);
|
||||
}
|
||||
|
||||
.search-result-widget-content .calendar-view {
|
||||
@ -85,17 +97,25 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
|
||||
/* #region Events */
|
||||
|
||||
.calendar-view a.fc-timegrid-event,
|
||||
.calendar-view a.fc-daygrid-event {
|
||||
.calendar-view a.fc-daygrid-event,
|
||||
.fc-daygrid-dot-event .fc-event-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-view a.fc-timegrid-event,
|
||||
.calendar-view a.fc-daygrid-event:not(.fc-daygrid-dot-event) {
|
||||
--border-color: transparent;
|
||||
.calendar-view a.fc-timegrid-event:focus-visible,
|
||||
.calendar-view a.fc-daygrid-event:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
border-width: 2px 2px 2px 4px;
|
||||
.calendar-view a.fc-timegrid-event,
|
||||
.calendar-view a.fc-daygrid-event {
|
||||
--border-color: transparent;
|
||||
|
||||
border: 2px solid;
|
||||
border-left-width: 4px;
|
||||
border-color: var(--border-color) var(--border-color) var(--border-color)
|
||||
var(--fc-event-text-color) !important;
|
||||
background: var(--fc-event-bg-color) !important;
|
||||
|
||||
padding-left: 8px;
|
||||
}
|
||||
@ -115,8 +135,8 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.fc-timegrid-event.with-hue,
|
||||
.fc-daygrid-event:not(.fc-daygrid-dot-event).with-hue {
|
||||
.calendar-view .fc-timegrid-event.with-hue,
|
||||
.calendar-view .fc-daygrid-event.with-hue {
|
||||
--fc-event-text-color: var(--custom-color);
|
||||
|
||||
background: hsl(var(--custom-color-hue),
|
||||
@ -124,8 +144,12 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
|
||||
var(--calendar-coll-event-background-lightness)) !important;
|
||||
}
|
||||
|
||||
.fc-event-time {
|
||||
.calendar-view .fc-event-time {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.fc-daygrid-event-dot {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@ -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 { SuggestionMode } 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,
|
||||
suggestionMode: SuggestionMode.SuggestCreateAndLink,
|
||||
hideAllButtons: true
|
||||
}}
|
||||
noteIdChanged={success}
|
||||
|
||||
@ -181,8 +181,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
||||
uiIcon: "bx bx-horizontal-left bx-rotate-90",
|
||||
enabled: !sorters.length,
|
||||
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
||||
parentNotePath: parentNoteId,
|
||||
customOpts: {
|
||||
parentNoteLink: parentNoteId,
|
||||
target: "before",
|
||||
targetBranchId: rowData.branchId,
|
||||
}
|
||||
@ -194,9 +194,12 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
||||
handler: async () => {
|
||||
const branchId = row.getData().branchId;
|
||||
const note = await froca.getBranch(branchId)?.getNote();
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
parentComponent?.triggerCommand("addNewRow", {
|
||||
parentNotePath: note?.noteId,
|
||||
customOpts: {
|
||||
parentNoteLink: note.noteId,
|
||||
target: "after",
|
||||
targetBranchId: branchId,
|
||||
}
|
||||
@ -208,8 +211,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
||||
uiIcon: "bx bx-horizontal-left bx-rotate-270",
|
||||
enabled: !sorters.length,
|
||||
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
||||
parentNotePath: parentNoteId,
|
||||
customOpts: {
|
||||
parentNoteLink: parentNoteId,
|
||||
target: "after",
|
||||
targetBranchId: rowData.branchId,
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables";
|
||||
import { CommandListenerData } from "../../../components/app_context";
|
||||
import note_create, { CreateNoteOpts } from "../../../services/note_create";
|
||||
import note_create from "../../../services/note_create";
|
||||
import { useLegacyImperativeHandlers } from "../../react/hooks";
|
||||
import { RefObject } from "preact";
|
||||
import { setAttribute, setLabel } from "../../../services/attributes";
|
||||
@ -9,17 +9,23 @@ import server from "../../../services/server";
|
||||
import branches from "../../../services/branches";
|
||||
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
||||
|
||||
/**
|
||||
* Hook for handling row table editing, including adding new rows.
|
||||
*/
|
||||
export default function useRowTableEditing(api: RefObject<Tabulator>, attributeDetailWidget: AttributeDetailWidget, parentNotePath: string): Partial<EventCallBackMethods> {
|
||||
// Adding new rows
|
||||
useLegacyImperativeHandlers({
|
||||
addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) {
|
||||
const notePath = customNotePath ?? parentNotePath;
|
||||
if (notePath) {
|
||||
const opts: CreateNoteOpts = {
|
||||
activate: false,
|
||||
...customOpts
|
||||
}
|
||||
note_create.createNote(notePath, opts).then(({ branch }) => {
|
||||
addNewRowCommand({ customOpts }: CommandListenerData<"addNewRow">) {
|
||||
if (!customOpts) {
|
||||
customOpts = {
|
||||
target: "into",
|
||||
};
|
||||
}
|
||||
|
||||
const noteUrl = customOpts.parentNoteLink ?? parentNotePath;
|
||||
if (noteUrl) {
|
||||
customOpts.parentNoteLink = noteUrl;
|
||||
customOpts.activate = false;
|
||||
note_create.createNote(customOpts).then(({ branch }) => {
|
||||
if (branch) {
|
||||
setTimeout(() => {
|
||||
if (!api.current) return;
|
||||
@ -27,6 +33,7 @@ export default function useRowTableEditing(api: RefObject<Tabulator>, attributeD
|
||||
}, 100);
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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, { SuggestionMode, Suggestion } from "../../services/note_autocomplete";
|
||||
import { logError } from "../../services/ws";
|
||||
import FormGroup from "../react/FormGroup.js";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
@ -58,7 +58,7 @@ export default function AddLinkDialog() {
|
||||
}
|
||||
|
||||
if (suggestion.notePath) {
|
||||
const noteId = tree.getNoteIdFromUrl(suggestion.notePath);
|
||||
const noteId = tree.getNoteIdFromLink(suggestion.notePath);
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
}
|
||||
@ -133,7 +133,7 @@ export default function AddLinkDialog() {
|
||||
onChange={setSuggestion}
|
||||
opts={{
|
||||
allowExternalLinks: true,
|
||||
allowCreatingNotes: true
|
||||
suggestionMode: SuggestionMode.SuggestCreateAndLink,
|
||||
}}
|
||||
/>
|
||||
</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 { SuggestionMode, Suggestion, triggerRecentNotes } from "../../services/note_autocomplete.js";
|
||||
import tree from "../../services/tree";
|
||||
import froca from "../../services/froca";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
@ -50,7 +50,7 @@ export default function IncludeNoteDialog() {
|
||||
inputRef={autoCompleteRef}
|
||||
opts={{
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true
|
||||
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
@ -71,7 +71,7 @@ export default function IncludeNoteDialog() {
|
||||
}
|
||||
|
||||
async function includeNote(notePath: string, editorApi: CKEditorApi, boxSize: BoxSize) {
|
||||
const noteId = tree.getNoteIdFromUrl(notePath);
|
||||
const noteId = tree.getNoteIdFromLink(notePath);
|
||||
if (!noteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -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, { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
|
||||
import appContext from "../../components/app_context";
|
||||
import commandRegistry from "../../services/command_registry";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
@ -12,34 +12,53 @@ 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;
|
||||
|
||||
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 +70,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);
|
||||
@ -70,12 +89,12 @@ export default function JumpToNoteDialogComponent() {
|
||||
function onShown() {
|
||||
const $autoComplete = refToJQuerySelector(autocompleteRef);
|
||||
switch (mode) {
|
||||
case "last-search":
|
||||
case Mode.LastSearch:
|
||||
break;
|
||||
case "recent-notes":
|
||||
case Mode.RecentNotes:
|
||||
note_autocomplete.showRecentNotes($autoComplete);
|
||||
break;
|
||||
case "commands":
|
||||
case Mode.Commands:
|
||||
note_autocomplete.showAllCommands($autoComplete);
|
||||
break;
|
||||
}
|
||||
@ -83,7 +102,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 +110,7 @@ export default function JumpToNoteDialogComponent() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function showInFullSearch() {
|
||||
try {
|
||||
setShown(false);
|
||||
@ -116,7 +135,7 @@ export default function JumpToNoteDialogComponent() {
|
||||
container={containerRef}
|
||||
text={initialText}
|
||||
opts={{
|
||||
allowCreatingNotes: true,
|
||||
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowJumpToSearchNotes: true,
|
||||
isCommandPalette: true
|
||||
@ -129,9 +148,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 { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
|
||||
import Badge from "../react/Badge";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
|
||||
@ -76,6 +76,7 @@ export default function NoteTypeChooserDialogComponent() {
|
||||
onHidden={() => {
|
||||
callback?.({ success: false });
|
||||
setShown(false);
|
||||
setParentNote(null);
|
||||
}}
|
||||
show={shown}
|
||||
stackable
|
||||
@ -85,7 +86,7 @@ export default function NoteTypeChooserDialogComponent() {
|
||||
onChange={setParentNote}
|
||||
placeholder={t("note_type_chooser.search_placeholder")}
|
||||
opts={{
|
||||
allowCreatingNotes: false,
|
||||
suggestionMode: SuggestionMode.SuggestNothing,
|
||||
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, { SuggestionMode } 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, SuggestionMode.SuggestCreateAndLink),
|
||||
itemRenderer: (item) => {
|
||||
const suggestion = item as Suggestion;
|
||||
const itemElement = document.createElement("button");
|
||||
|
||||
@ -29,7 +29,14 @@ export default function MobileDetailMenu() {
|
||||
],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
const parentNoteUrl = appContext.tabManager.getActiveContextNotePath();
|
||||
|
||||
if (parentNoteUrl) {
|
||||
note_create.createNote({
|
||||
target: "into",
|
||||
parentNoteLink: parentNoteUrl,
|
||||
});
|
||||
}
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
|
||||
@ -224,7 +224,13 @@ 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.createNote(
|
||||
{
|
||||
target: "into",
|
||||
parentNoteLink: 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 });
|
||||
@ -1403,10 +1409,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
let node: Fancytree.FancytreeNode | null | undefined = await this.expandToNote(activeNotePath, false);
|
||||
|
||||
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) {
|
||||
if (node && node.data.noteId !== treeService.getNoteIdFromLink(activeNotePath)) {
|
||||
// if the active note has been moved elsewhere then it won't be found by the path,
|
||||
// so we switch to the alternative of trying to find it by noteId
|
||||
const noteId = treeService.getNoteIdFromUrl(activeNotePath);
|
||||
const noteId = treeService.getNoteIdFromLink(activeNotePath);
|
||||
|
||||
if (noteId) {
|
||||
const notesById = this.getNodesByNoteId(noteId);
|
||||
@ -1836,9 +1842,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const node = this.getActiveNode();
|
||||
if (!node) return;
|
||||
const notePath = treeService.getNotePath(node);
|
||||
noteCreateService.createNote(notePath, {
|
||||
isProtected: node.data.isProtected
|
||||
});
|
||||
noteCreateService.createNote(
|
||||
{
|
||||
target: "into",
|
||||
parentNoteLink: notePath,
|
||||
isProtected: node.data.isProtected
|
||||
}
|
||||
)
|
||||
}
|
||||
}),
|
||||
new TouchBar.TouchBarButton({
|
||||
|
||||
@ -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, { SuggestionMode, Suggestion } from "../../../services/note_autocomplete.js";
|
||||
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 { CreateNoteAction } from "@triliumnext/commons";
|
||||
|
||||
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, SuggestionMode.SuggestCreateAndLink),
|
||||
itemRenderer: (_item) => {
|
||||
const item = _item as Suggestion;
|
||||
const itemElement = document.createElement("button");
|
||||
@ -247,16 +248,18 @@ 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
|
||||
});
|
||||
}
|
||||
|
||||
return result?.note?.getBestNotePathString();
|
||||
createNoteFromCkEditor: async (
|
||||
title: string,
|
||||
parentNotePath: string | undefined,
|
||||
action: CreateNoteAction
|
||||
): Promise<string> => {
|
||||
const { note } = await note_create.createNoteFromAction(
|
||||
action,
|
||||
true,
|
||||
title,
|
||||
parentNotePath,
|
||||
);
|
||||
return note?.getBestNotePathString() ?? "";
|
||||
}
|
||||
}), [ notePath ]));
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import FormGroup from "../react/FormGroup";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
import "./Empty.css";
|
||||
import { ParentComponent, refToJQuerySelector } from "../react/react_utils";
|
||||
import note_autocomplete from "../../services/note_autocomplete";
|
||||
import note_autocomplete, { SuggestionMode } from "../../services/note_autocomplete";
|
||||
import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import search from "../../services/search";
|
||||
@ -38,7 +38,7 @@ function NoteSearch({ ntxId }: { ntxId: string | null }) {
|
||||
inputRef={autocompleteRef}
|
||||
opts={{
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true,
|
||||
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||
allowJumpToSearchNotes: true,
|
||||
}}
|
||||
onChange={suggestion => {
|
||||
|
||||
@ -16,7 +16,7 @@ import note_create from "../../../services/note_create";
|
||||
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
|
||||
import { RefObject } from "preact";
|
||||
import { buildSelectedBackgroundColor } from "../../../components/touch_bar";
|
||||
import { deferred } from "@triliumnext/commons";
|
||||
import { CreateNoteAction, deferred } from "@triliumnext/commons";
|
||||
import { t } from "../../../services/i18n";
|
||||
|
||||
/**
|
||||
@ -115,17 +115,18 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
},
|
||||
loadIncludedNote,
|
||||
// Creating notes in @-completion
|
||||
async createNoteForReferenceLink(title: string) {
|
||||
const notePath = noteContext?.notePath;
|
||||
if (!notePath) return;
|
||||
|
||||
const resp = await note_create.createNoteWithTypePrompt(notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
|
||||
if (!resp || !resp.note) return;
|
||||
return resp.note.getBestNotePathString();
|
||||
async createNoteFromCkEditor (
|
||||
title: string,
|
||||
parentNotePath: string | undefined,
|
||||
action: CreateNoteAction
|
||||
): Promise<string> {
|
||||
const { note }= await note_create.createNoteFromAction(
|
||||
action,
|
||||
true,
|
||||
title,
|
||||
parentNotePath,
|
||||
)
|
||||
return note?.getBestNotePathString() ?? "";
|
||||
},
|
||||
// Keyboard shortcut
|
||||
async followLinkUnderCursorCommand() {
|
||||
@ -162,7 +163,9 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
// without await as this otherwise causes deadlock through component mutex
|
||||
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (noteContext && parentNotePath) {
|
||||
note_create.createNote(parentNotePath, {
|
||||
note_create.createNote({
|
||||
parentNoteLink: parentNotePath,
|
||||
target: "into",
|
||||
isProtected: note.isProtected,
|
||||
saveSelection: true,
|
||||
textEditor: await noteContext?.getTextEditor()
|
||||
|
||||
@ -7,7 +7,7 @@ import emojiDefinitionsUrl from "@triliumnext/ckeditor5/src/emoji_definitions/en
|
||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import { getMermaidConfig } from "../../../services/mermaid.js";
|
||||
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
|
||||
import noteAutocompleteService, { SuggestionMode, 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, SuggestionMode.SuggestCreateAndLink),
|
||||
itemRenderer: (item) => {
|
||||
const itemElement = document.createElement("button");
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import path from "path";
|
||||
import path, { join } from "path";
|
||||
import fs from "fs-extra";
|
||||
import { LOCALES } from "@triliumnext/commons";
|
||||
import { PRODUCT_NAME } from "../src/app-info.js";
|
||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const ELECTRON_FORGE_DIR = __dirname;
|
||||
|
||||
@ -228,8 +229,22 @@ const config: ForgeConfig = {
|
||||
// Ensure all locales that should be kept are actually present.
|
||||
for (const locale of localesToKeep) {
|
||||
if (!keptLocales.has(locale)) {
|
||||
console.error(`Locale ${locale} was not found in the packaged app.`);
|
||||
process.exit(1);
|
||||
throw new Error(`Locale ${locale} was not found in the packaged app.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the bettersqlite3 binary has the right architecture.
|
||||
if (packageResult.platform === "linux" && packageResult.arch === "arm64") {
|
||||
for (const outputPath of packageResult.outputPaths) {
|
||||
const binaryPath = join(outputPath, "resources/app.asar.unpacked/node_modules/better-sqlite3/build/Release/better_sqlite3.node");
|
||||
if (!existsSync(binaryPath)) {
|
||||
throw new Error(`[better-sqlite3] Unable to find .node file at ${binaryPath}`);
|
||||
}
|
||||
|
||||
const actualArch = getELFArch(binaryPath);
|
||||
if (actualArch !== "ARM64") {
|
||||
throw new Error(`[better-sqlite3] Expected ARM64 architecture but got ${actualArch} at: ${binaryPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -284,4 +299,20 @@ function getExtraResourcesForPlatform() {
|
||||
return resources;
|
||||
}
|
||||
|
||||
function getELFArch(file: string) {
|
||||
const buf = fs.readFileSync(file);
|
||||
|
||||
if (buf[0] !== 0x7f || buf[1] !== 0x45 || buf[2] !== 0x4c || buf[3] !== 0x46) {
|
||||
throw new Error("Not an ELF file");
|
||||
}
|
||||
|
||||
const eiClass = buf[4]; // 1=32-bit, 2=64-bit
|
||||
const eiMachine = buf[18]; // architecture code
|
||||
|
||||
if (eiMachine === 0x3E) return 'x86-64';
|
||||
if (eiMachine === 0xB7) return 'ARM64';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
|
||||
export default config;
|
||||
|
||||
@ -8,6 +8,7 @@ interface GotoOpts {
|
||||
}
|
||||
|
||||
const BASE_URL = "http://127.0.0.1:8082";
|
||||
const NUM_OF_CREATE_NOTE_OPTIONS = 2;
|
||||
|
||||
interface DropdownLocator extends Locator {
|
||||
selectOptionByText: (text: string) => Promise<void>;
|
||||
@ -73,7 +74,8 @@ export default class App {
|
||||
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
|
||||
await expect(resultsSelector).toContainText(noteTitle);
|
||||
await resultsSelector.locator(".aa-suggestion", { hasText: noteTitle })
|
||||
.nth(1) // Select the second one, as the first one is "Create a new note"
|
||||
// Select the n+1 one, as the first one is "Create a new note"
|
||||
.nth(NUM_OF_CREATE_NOTE_OPTIONS)
|
||||
.click();
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import "ckeditor5";
|
||||
import { type CreateNoteAction } from "@triliumnext/commons"
|
||||
|
||||
declare global {
|
||||
interface Component {
|
||||
@ -7,7 +8,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: CreateNoteAction): Promise<string>;
|
||||
loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>): void;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "ckeditor5";
|
||||
import { CreateNoteAction } from "@triliumnext/commons"
|
||||
|
||||
/**
|
||||
* 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?: CreateNoteAction;
|
||||
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 === CreateNoteAction.CreateNote ||
|
||||
mention.action === CreateNoteAction.CreateChildNote ||
|
||||
mention.action === CreateNoteAction.CreateAndLinkNote ||
|
||||
mention.action === CreateNoteAction.CreateAndLinkChildNote
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -10,4 +10,5 @@ export * from "./lib/server_api.js";
|
||||
export * from "./lib/shared_constants.js";
|
||||
export * from "./lib/ws_api.js";
|
||||
export * from "./lib/attribute_names.js";
|
||||
export * from "./lib/create_note_actions.js";
|
||||
export * from "./lib/utils.js";
|
||||
|
||||
6
packages/commons/src/lib/create_note_actions.ts
Normal file
6
packages/commons/src/lib/create_note_actions.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum CreateNoteAction {
|
||||
CreateNote = "create-note",
|
||||
CreateChildNote = "create-child-note",
|
||||
CreateAndLinkNote = "create-and-link-note",
|
||||
CreateAndLinkChildNote = "create-and-link-child-note"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user