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 { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||||
import type CodeMirror from "@triliumnext/codemirror";
|
import type CodeMirror from "@triliumnext/codemirror";
|
||||||
import { StartupChecks } from "./startup_checks.js";
|
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 { ColumnComponent } from "tabulator-tables";
|
||||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||||
import type RootContainer from "../widgets/containers/root_container.js";
|
import type RootContainer from "../widgets/containers/root_container.js";
|
||||||
@ -359,8 +359,7 @@ export type CommandMappings = {
|
|||||||
|
|
||||||
// Table view
|
// Table view
|
||||||
addNewRow: CommandData & {
|
addNewRow: CommandData & {
|
||||||
customOpts: CreateNoteOpts;
|
customOpts?: CreateNoteWithLinkOpts;
|
||||||
parentNotePath?: string;
|
|
||||||
};
|
};
|
||||||
addNewTableColumn: CommandData & {
|
addNewTableColumn: CommandData & {
|
||||||
columnToEdit?: ColumnComponent;
|
columnToEdit?: ColumnComponent;
|
||||||
|
|||||||
@ -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.js";
|
||||||
|
|
||||||
export default class Entrypoints extends Component {
|
export default class Entrypoints extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -24,23 +25,9 @@ export default class Entrypoints extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createNoteIntoInboxCommand() {
|
async createNoteIntoInboxCommand() {
|
||||||
const inboxNote = await dateNoteService.getInboxNote();
|
await noteCreateService.createNote(
|
||||||
if (!inboxNote) {
|
{ target: "default" }
|
||||||
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,10 +48,15 @@ export default class MainTreeExecutors extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(activeNoteContext.notePath, {
|
await noteCreateService.createNote(
|
||||||
isProtected: activeNoteContext.note.isProtected,
|
{
|
||||||
saveSelection: false
|
target: "into",
|
||||||
});
|
parentNoteLink: activeNoteContext.notePath,
|
||||||
|
isProtected: activeNoteContext.note.isProtected,
|
||||||
|
saveSelection: false,
|
||||||
|
promptForType: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteAfterCommand() {
|
async createNoteAfterCommand() {
|
||||||
@ -72,11 +77,14 @@ export default class MainTreeExecutors extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(parentNotePath, {
|
await noteCreateService.createNote(
|
||||||
target: "after",
|
{
|
||||||
targetBranchId: node.data.branchId,
|
target: "after",
|
||||||
isProtected: isProtected,
|
parentNoteLink: parentNotePath,
|
||||||
saveSelection: false
|
targetBranchId: node.data.branchId,
|
||||||
});
|
isProtected: isProtected,
|
||||||
|
saveSelection: false
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
||||||
const noteId = treeService.getNoteIdFromUrl(notePath);
|
const noteId = treeService.getNoteIdFromLink(notePath);
|
||||||
|
|
||||||
this.searchNotesCommand({ ancestorNoteId: noteId });
|
this.searchNotesCommand({ ancestorNoteId: noteId });
|
||||||
}
|
}
|
||||||
@ -240,14 +240,18 @@ 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.createNote(
|
||||||
title: "New AI Chat",
|
{
|
||||||
type: "aiChat",
|
parentNoteLink: rootNoteId,
|
||||||
content: JSON.stringify({
|
target: "into",
|
||||||
messages: [],
|
title: "New AI Chat",
|
||||||
title: "New AI Chat"
|
type: "aiChat",
|
||||||
})
|
content: JSON.stringify({
|
||||||
});
|
messages: [],
|
||||||
|
title: "New AI Chat"
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!result.note) {
|
if (!result.note) {
|
||||||
toastService.showError("Failed to create AI Chat note");
|
toastService.showError("Failed to create AI Chat note");
|
||||||
|
|||||||
@ -74,10 +74,10 @@ export default class TabManager extends Component {
|
|||||||
|
|
||||||
// preload all notes at once
|
// preload all notes at once
|
||||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
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 filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => {
|
||||||
const noteId = treeService.getNoteIdFromUrl(openTab.notePath);
|
const noteId = treeService.getNoteIdFromLink(openTab.notePath);
|
||||||
if (!noteId || !(noteId in froca.notes)) {
|
if (!noteId || !(noteId in froca.notes)) {
|
||||||
// note doesn't exist so don't try to open tab for it
|
// note doesn't exist so don't try to open tab for it
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -283,21 +283,31 @@ 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, {
|
|
||||||
target: "after",
|
noteCreateService.createNote(
|
||||||
targetBranchId: this.node.data.branchId,
|
{
|
||||||
type: type,
|
target: "after",
|
||||||
isProtected: isProtected,
|
parentNoteLink: parentNotePath,
|
||||||
templateNoteId: templateNoteId
|
targetBranchId: this.node.data.branchId,
|
||||||
});
|
type: type,
|
||||||
|
isProtected: isProtected,
|
||||||
|
templateNoteId: templateNoteId,
|
||||||
|
promptForType: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else if (command === "insertChildNote") {
|
} else if (command === "insertChildNote") {
|
||||||
const parentNotePath = treeService.getNotePath(this.node);
|
const parentNotePath = treeService.getNotePath(this.node);
|
||||||
|
|
||||||
noteCreateService.createNote(parentNotePath, {
|
noteCreateService.createNote(
|
||||||
type: type,
|
{
|
||||||
isProtected: this.node.data.isProtected,
|
target: "into",
|
||||||
templateNoteId: templateNoteId
|
parentNoteLink: parentNotePath,
|
||||||
});
|
type: type,
|
||||||
|
isProtected: this.node.data.isProtected,
|
||||||
|
templateNoteId: templateNoteId,
|
||||||
|
promptForType: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else if (command === "openNoteInSplit") {
|
} else if (command === "openNoteInSplit") {
|
||||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||||
|
|||||||
@ -50,7 +50,7 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
|
|||||||
const hoistedNoteId = noteContext.hoistedNoteId;
|
const hoistedNoteId = noteContext.hoistedNoteId;
|
||||||
|
|
||||||
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) {
|
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) {
|
||||||
const noteId = treeService.getNoteIdFromUrl(resolvedNotePath);
|
const noteId = treeService.getNoteIdFromLink(resolvedNotePath);
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,7 +261,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
notePath,
|
notePath,
|
||||||
noteId: treeService.getNoteIdFromUrl(notePath),
|
noteId: treeService.getNoteIdFromLink(notePath),
|
||||||
ntxId,
|
ntxId,
|
||||||
hoistedNoteId,
|
hoistedNoteId,
|
||||||
viewScope,
|
viewScope,
|
||||||
|
|||||||
@ -5,6 +5,24 @@ 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 { 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
|
// 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 +41,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 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 {
|
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 +86,7 @@ export interface Suggestion {
|
|||||||
export interface Options {
|
export interface Options {
|
||||||
container?: HTMLElement | null;
|
container?: HTMLElement | null;
|
||||||
fastSearch?: boolean;
|
fastSearch?: boolean;
|
||||||
allowCreatingNotes?: boolean;
|
suggestionMode?: SuggestionMode;
|
||||||
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 +97,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,
|
||||||
|
suggestionMode: SuggestionMode
|
||||||
|
): 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
|
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
|
// 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.suggestionMode) {
|
||||||
action: "create-note",
|
case SuggestionMode.SuggestCreateOnly: {
|
||||||
noteTitle: term,
|
results = [
|
||||||
parentNoteId: activeNoteId || "root",
|
{
|
||||||
highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
|
action: SuggestionAction.CreateNote,
|
||||||
} as Suggestion
|
noteTitle: trimmedTerm,
|
||||||
].concat(results);
|
parentNoteId: "inbox",
|
||||||
}
|
highlightedNotePathTitle: t("note_autocomplete.create-note", { term: trimmedTerm }),
|
||||||
|
},
|
||||||
if (length >= 1 && options.allowJumpToSearchNotes) {
|
{
|
||||||
results = results.concat([
|
action: SuggestionAction.CreateChildNote,
|
||||||
{
|
noteTitle: trimmedTerm,
|
||||||
action: "search-notes",
|
parentNoteId: activeNoteId || "root",
|
||||||
noteTitle: term,
|
highlightedNotePathTitle: t("note_autocomplete.create-child-note", { 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 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 = [
|
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 +291,85 @@ 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.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) {
|
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 +455,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 +477,85 @@ 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") {
|
async function doCommand() {
|
||||||
$el.autocomplete("close");
|
$el.autocomplete("close");
|
||||||
$el.trigger("autocomplete:commandselected", [suggestion]);
|
$el.trigger("autocomplete:commandselected", [suggestion]);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.action === "external-link") {
|
async function doExternalLink() {
|
||||||
$el.setSelectedNotePath(null);
|
$el.setSelectedNotePath(null);
|
||||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||||
|
|
||||||
$el.autocomplete("val", 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.autocomplete("close");
|
||||||
|
|
||||||
$el.trigger("autocomplete:externallinkselected", [suggestion]);
|
$el.trigger("autocomplete:noteselected", [suggestion]);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.action === "create-note") {
|
switch (suggestion.action) {
|
||||||
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
|
case SuggestionAction.Command:
|
||||||
if (!success) {
|
await doCommand();
|
||||||
return;
|
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;
|
case SuggestionAction.SearchNotes:
|
||||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
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", () => {
|
$el.on("autocomplete:closed", () => {
|
||||||
|
|||||||
@ -10,8 +10,63 @@ 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 { 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;
|
isProtected?: boolean;
|
||||||
saveSelection?: boolean;
|
saveSelection?: boolean;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@ -21,10 +76,34 @@ export interface CreateNoteOpts {
|
|||||||
templateNoteId?: string;
|
templateNoteId?: string;
|
||||||
activate?: boolean;
|
activate?: boolean;
|
||||||
focus?: "title" | "content";
|
focus?: "title" | "content";
|
||||||
target?: string;
|
|
||||||
targetBranchId?: string;
|
|
||||||
textEditor?: CKTextEditor;
|
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 {
|
interface Response {
|
||||||
// TODO: Deduplicate with server once we have client/server architecture.
|
// TODO: Deduplicate with server once we have client/server architecture.
|
||||||
@ -37,7 +116,141 @@ interface DuplicateResponse {
|
|||||||
note: FNote;
|
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(
|
options = Object.assign(
|
||||||
{
|
{
|
||||||
activate: true,
|
activate: true,
|
||||||
@ -61,7 +274,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
|||||||
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
|
[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) {
|
if (options.type === "mermaid" && !options.content && !options.templateNoteId) {
|
||||||
options.content = `graph TD;
|
options.content = `graph TD;
|
||||||
@ -71,7 +285,12 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
|||||||
C-->D;`;
|
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,
|
title: options.title,
|
||||||
content: options.content || "",
|
content: options.content || "",
|
||||||
isProtected: options.isProtected,
|
isProtected: options.isProtected,
|
||||||
@ -89,7 +308,7 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
|
|||||||
|
|
||||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||||
if (activeNoteContext && options.activate) {
|
if (activeNoteContext && options.activate) {
|
||||||
await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
|
await activeNoteContext.setNote(`${parentNoteId}/${note.noteId}`);
|
||||||
|
|
||||||
if (options.focus === "title") {
|
if (options.focus === "title") {
|
||||||
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
|
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() {
|
async function chooseNoteType() {
|
||||||
return new Promise<ChooseNoteTypeResponse>((res) => {
|
return new Promise<ChooseNoteTypeResponse>((res) => {
|
||||||
appContext.triggerCommand("chooseNoteType", { callback: 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. */
|
/* If the first element is heading, parse it out and use it as a new heading. */
|
||||||
function parseSelectedHtml(selectedHtml: string) {
|
function parseSelectedHtml(selectedHtml: string) {
|
||||||
const dom = $.parseHTML(selectedHtml);
|
const dom = $.parseHTML(selectedHtml);
|
||||||
@ -146,7 +386,7 @@ function parseSelectedHtml(selectedHtml: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateSubtree(noteId: string, parentNotePath: 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}`);
|
const { note } = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
@ -159,7 +399,6 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
createNote,
|
createNote,
|
||||||
createNoteWithTypePrompt,
|
createNoteFromAction,
|
||||||
duplicateSubtree,
|
duplicateSubtree,
|
||||||
chooseNoteType
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,7 +92,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
|
|||||||
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
|
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
|
||||||
return effectivePathSegments;
|
return effectivePathSegments;
|
||||||
} else {
|
} else {
|
||||||
const noteId = getNoteIdFromUrl(notePath);
|
const noteId = getNoteIdFromLink(notePath);
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
throw new Error(`Unable to find note with ID: ${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;
|
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteIdFromUrl(urlOrNotePath: string | null | undefined) {
|
function getNoteIdFromLink(urlOrNotePath: string | null | undefined) {
|
||||||
if (!urlOrNotePath) {
|
if (!urlOrNotePath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -306,7 +306,7 @@ export default {
|
|||||||
getParentProtectedStatus,
|
getParentProtectedStatus,
|
||||||
getNotePath,
|
getNotePath,
|
||||||
getNotePathTitleComponents,
|
getNotePathTitleComponents,
|
||||||
getNoteIdFromUrl,
|
getNoteIdFromLink,
|
||||||
getNoteIdAndParentIdFromUrl,
|
getNoteIdAndParentIdFromUrl,
|
||||||
getBranchIdFromUrl,
|
getBranchIdFromUrl,
|
||||||
getNoteTitle,
|
getNoteTitle,
|
||||||
|
|||||||
@ -27,6 +27,9 @@
|
|||||||
--bs-body-bg: var(--main-background-color) !important;
|
--bs-body-bg: var(--main-background-color) !important;
|
||||||
--ck-mention-list-max-height: 500px;
|
--ck-mention-list-max-height: 500px;
|
||||||
--tn-modal-max-height: 90vh;
|
--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 *,
|
body#trilium-app.motion-disabled *,
|
||||||
@ -2579,4 +2582,12 @@ iframe.print-iframe {
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 100%;
|
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;
|
--mermaid-theme: dark;
|
||||||
--native-titlebar-background: #00000000;
|
--native-titlebar-background: #00000000;
|
||||||
|
|
||||||
|
--calendar-coll-event-background-saturation: 30%;
|
||||||
|
--calendar-coll-event-background-lightness: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::-webkit-calendar-picker-indicator {
|
body ::-webkit-calendar-picker-indicator {
|
||||||
|
|||||||
@ -80,6 +80,9 @@ html {
|
|||||||
|
|
||||||
--mermaid-theme: default;
|
--mermaid-theme: default;
|
||||||
--native-titlebar-background: #ffffff00;
|
--native-titlebar-background: #ffffff00;
|
||||||
|
|
||||||
|
--calendar-coll-event-background-lightness: 95%;
|
||||||
|
--calendar-coll-event-background-saturation: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left-pane .fancytree-node.tinted {
|
#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-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
|
||||||
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
|
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
|
||||||
|
|
||||||
--calendar-coll-event-background-saturation: 12%;
|
--calendar-coll-event-background-saturation: 25%;
|
||||||
--calendar-coll-event-background-lightness: 21%;
|
--calendar-coll-event-background-lightness: 20%;
|
||||||
--calendar-coll-event-background-color: #3c3c3c;
|
--calendar-coll-event-background-color: #3c3c3c;
|
||||||
--calendar-coll-event-text-color: white;
|
--calendar-coll-event-text-color: white;
|
||||||
--calendar-cell-event-hover-filter: brightness(1.25);
|
--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-background-color: #eaeaea;
|
||||||
--calendar-coll-event-text-color: black;
|
--calendar-coll-event-text-color: black;
|
||||||
--calendar-cell-event-hover-filter: brightness(.95) saturate(1.25);
|
--calendar-cell-event-hover-filter: brightness(.95) saturate(1.25);
|
||||||
|
--calendar-coll-today-background-color: #00000006;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left-pane .fancytree-node.tinted {
|
#left-pane .fancytree-node.tinted {
|
||||||
|
|||||||
@ -1897,7 +1897,10 @@
|
|||||||
},
|
},
|
||||||
"note_autocomplete": {
|
"note_autocomplete": {
|
||||||
"search-for": "Search for \"{{term}}\"",
|
"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}}\"",
|
"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, { SuggestionMode } 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, { suggestionMode: SuggestionMode.SuggestCreateAndLink }).on("autocomplete:noteselected", (event, suggestion, dataset) => {
|
||||||
if (!suggestion.notePath) {
|
if (!suggestion.notePath) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import branches from "../../../services/branches";
|
|||||||
import { executeBulkActions } from "../../../services/bulk_action";
|
import { executeBulkActions } from "../../../services/bulk_action";
|
||||||
import froca from "../../../services/froca";
|
import froca from "../../../services/froca";
|
||||||
import { t } from "../../../services/i18n";
|
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 server from "../../../services/server";
|
||||||
import { ColumnMap } from "./data";
|
import { ColumnMap } from "./data";
|
||||||
|
|
||||||
@ -39,9 +39,11 @@ 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.createNote({
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
activate: false,
|
activate: false,
|
||||||
title
|
title,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newNote && newBranch) {
|
if (newNote && newBranch) {
|
||||||
@ -139,13 +141,17 @@ export default class BoardApi {
|
|||||||
async insertRowAtPosition(
|
async insertRowAtPosition(
|
||||||
column: string,
|
column: string,
|
||||||
relativeToBranchId: string,
|
relativeToBranchId: string,
|
||||||
direction: "before" | "after") {
|
direction: "before" | "after"
|
||||||
const { note, branch } = await note_create.createNote(this.parentNote.noteId, {
|
) {
|
||||||
activate: false,
|
const { note, branch } = await note_create.createNote(
|
||||||
targetBranchId: relativeToBranchId,
|
{
|
||||||
target: direction,
|
target: direction,
|
||||||
title: t("board_view.new-item")
|
parentNoteLink: this.parentNote.noteId,
|
||||||
});
|
activate: false,
|
||||||
|
targetBranchId: relativeToBranchId,
|
||||||
|
title: t("board_view.new-item"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!note || !branch) {
|
if (!note || !branch) {
|
||||||
throw new Error("Failed to create note");
|
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"),
|
title: t("board_view.insert-above"),
|
||||||
uiIcon: "bx bx-list-plus",
|
uiIcon: "bx bx-list-plus",
|
||||||
handler: () => api.insertRowAtPosition(column, branchId, "before")
|
handler: () => api.insertRowAtPosition(
|
||||||
|
column,
|
||||||
|
branchId,
|
||||||
|
"before")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("board_view.insert-below"),
|
title: t("board_view.insert-below"),
|
||||||
uiIcon: "bx bx-empty",
|
uiIcon: "bx bx-empty",
|
||||||
handler: () => api.insertRowAtPosition(column, branchId, "after")
|
handler: () => api.insertRowAtPosition(
|
||||||
|
column,
|
||||||
|
branchId,
|
||||||
|
"after")
|
||||||
},
|
},
|
||||||
{ kind: "separator" },
|
{ kind: "separator" },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import FormTextArea from "../../react/FormTextArea";
|
|||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
import NoteAutocomplete from "../../react/NoteAutocomplete";
|
import NoteAutocomplete from "../../react/NoteAutocomplete";
|
||||||
import toast from "../../../services/toast";
|
import toast from "../../../services/toast";
|
||||||
|
import { SuggestionMode } from "../../../services/note_autocomplete";
|
||||||
|
|
||||||
export interface BoardViewData {
|
export interface BoardViewData {
|
||||||
columns?: BoardColumnData[];
|
columns?: BoardColumnData[];
|
||||||
@ -309,7 +310,7 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
|
|||||||
noteId={currentValue ?? ""}
|
noteId={currentValue ?? ""}
|
||||||
opts={{
|
opts={{
|
||||||
hideAllButtons: true,
|
hideAllButtons: true,
|
||||||
allowCreatingNotes: true
|
suggestionMode: SuggestionMode.SuggestCreateAndLink
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape") {
|
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) {
|
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) {
|
||||||
const customTitleAttributeName = note.getLabelValue("calendar:title");
|
const customTitleAttributeName = note.getLabelValue("calendar:title");
|
||||||
const titles = await parseCustomTitle(customTitleAttributeName, note);
|
const titles = await parseCustomTitle(customTitleAttributeName, note);
|
||||||
const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color");
|
|
||||||
const colorClass = note.getColorClass();
|
const colorClass = note.getColorClass();
|
||||||
const events: EventInput[] = [];
|
const events: EventInput[] = [];
|
||||||
|
|
||||||
@ -110,7 +109,6 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
|
|||||||
start: startDate,
|
start: startDate,
|
||||||
url: `#${note.noteId}?popup`,
|
url: `#${note.noteId}?popup`,
|
||||||
noteId: note.noteId,
|
noteId: note.noteId,
|
||||||
color: color ?? undefined,
|
|
||||||
iconClass: note.getLabelValue("iconClass"),
|
iconClass: note.getLabelValue("iconClass"),
|
||||||
promotedAttributes: displayedAttributesData,
|
promotedAttributes: displayedAttributesData,
|
||||||
className: clsx({archived: isArchived}, colorClass)
|
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 {
|
.calendar-view {
|
||||||
--fc-event-border-color: var(--calendar-coll-event-text-color);
|
--fc-event-border-color: var(--calendar-coll-event-text-color);
|
||||||
--fc-event-bg-color: var(--calendar-coll-event-background-color);
|
--fc-event-bg-color: var(--calendar-coll-event-background-color);
|
||||||
--fc-event-text-color: var(--calendar-coll-event-text-color);
|
--fc-event-text-color: var(--calendar-coll-event-text-color);
|
||||||
--fc-event-selected-overlay-color: transparent;
|
--fc-event-selected-overlay-color: transparent;
|
||||||
|
--fc-today-bg-color: var(--calendar-coll-today-background-color);
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -12,8 +23,9 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-view a {
|
.calendar-view a,
|
||||||
color: unset;
|
:root .calendar-view a.fc-daygrid-event:hover {
|
||||||
|
color: var(--fc-event-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-result-widget-content .calendar-view {
|
.search-result-widget-content .calendar-view {
|
||||||
@ -85,17 +97,25 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
|
|||||||
/* #region Events */
|
/* #region Events */
|
||||||
|
|
||||||
.calendar-view a.fc-timegrid-event,
|
.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;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-view a.fc-timegrid-event,
|
.calendar-view a.fc-timegrid-event:focus-visible,
|
||||||
.calendar-view a.fc-daygrid-event:not(.fc-daygrid-dot-event) {
|
.calendar-view a.fc-daygrid-event:focus-visible {
|
||||||
--border-color: transparent;
|
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)
|
border-color: var(--border-color) var(--border-color) var(--border-color)
|
||||||
var(--fc-event-text-color) !important;
|
var(--fc-event-text-color) !important;
|
||||||
|
background: var(--fc-event-bg-color) !important;
|
||||||
|
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
@ -115,8 +135,8 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
|
|||||||
color: currentColor;
|
color: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-timegrid-event.with-hue,
|
.calendar-view .fc-timegrid-event.with-hue,
|
||||||
.fc-daygrid-event:not(.fc-daygrid-dot-event).with-hue {
|
.calendar-view .fc-daygrid-event.with-hue {
|
||||||
--fc-event-text-color: var(--custom-color);
|
--fc-event-text-color: var(--custom-color);
|
||||||
|
|
||||||
background: hsl(var(--custom-color-hue),
|
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;
|
var(--calendar-coll-event-background-lightness)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-event-time {
|
.calendar-view .fc-event-time {
|
||||||
opacity: .75;
|
opacity: .75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-event-dot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
@ -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 { SuggestionMode } 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,
|
suggestionMode: SuggestionMode.SuggestCreateAndLink,
|
||||||
hideAllButtons: true
|
hideAllButtons: true
|
||||||
}}
|
}}
|
||||||
noteIdChanged={success}
|
noteIdChanged={success}
|
||||||
|
|||||||
@ -181,8 +181,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
|||||||
uiIcon: "bx bx-horizontal-left bx-rotate-90",
|
uiIcon: "bx bx-horizontal-left bx-rotate-90",
|
||||||
enabled: !sorters.length,
|
enabled: !sorters.length,
|
||||||
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
||||||
parentNotePath: parentNoteId,
|
|
||||||
customOpts: {
|
customOpts: {
|
||||||
|
parentNoteLink: parentNoteId,
|
||||||
target: "before",
|
target: "before",
|
||||||
targetBranchId: rowData.branchId,
|
targetBranchId: rowData.branchId,
|
||||||
}
|
}
|
||||||
@ -194,9 +194,12 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
|||||||
handler: async () => {
|
handler: async () => {
|
||||||
const branchId = row.getData().branchId;
|
const branchId = row.getData().branchId;
|
||||||
const note = await froca.getBranch(branchId)?.getNote();
|
const note = await froca.getBranch(branchId)?.getNote();
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
parentComponent?.triggerCommand("addNewRow", {
|
parentComponent?.triggerCommand("addNewRow", {
|
||||||
parentNotePath: note?.noteId,
|
|
||||||
customOpts: {
|
customOpts: {
|
||||||
|
parentNoteLink: note.noteId,
|
||||||
target: "after",
|
target: "after",
|
||||||
targetBranchId: branchId,
|
targetBranchId: branchId,
|
||||||
}
|
}
|
||||||
@ -208,8 +211,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
|
|||||||
uiIcon: "bx bx-horizontal-left bx-rotate-270",
|
uiIcon: "bx bx-horizontal-left bx-rotate-270",
|
||||||
enabled: !sorters.length,
|
enabled: !sorters.length,
|
||||||
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
handler: () => parentComponent?.triggerCommand("addNewRow", {
|
||||||
parentNotePath: parentNoteId,
|
|
||||||
customOpts: {
|
customOpts: {
|
||||||
|
parentNoteLink: parentNoteId,
|
||||||
target: "after",
|
target: "after",
|
||||||
targetBranchId: rowData.branchId,
|
targetBranchId: rowData.branchId,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables";
|
import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables";
|
||||||
import { CommandListenerData } from "../../../components/app_context";
|
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 { useLegacyImperativeHandlers } from "../../react/hooks";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
import { setAttribute, setLabel } from "../../../services/attributes";
|
import { setAttribute, setLabel } from "../../../services/attributes";
|
||||||
@ -9,17 +9,23 @@ import server from "../../../services/server";
|
|||||||
import branches from "../../../services/branches";
|
import branches from "../../../services/branches";
|
||||||
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
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> {
|
export default function useRowTableEditing(api: RefObject<Tabulator>, attributeDetailWidget: AttributeDetailWidget, parentNotePath: string): Partial<EventCallBackMethods> {
|
||||||
// Adding new rows
|
|
||||||
useLegacyImperativeHandlers({
|
useLegacyImperativeHandlers({
|
||||||
addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) {
|
addNewRowCommand({ customOpts }: CommandListenerData<"addNewRow">) {
|
||||||
const notePath = customNotePath ?? parentNotePath;
|
if (!customOpts) {
|
||||||
if (notePath) {
|
customOpts = {
|
||||||
const opts: CreateNoteOpts = {
|
target: "into",
|
||||||
activate: false,
|
};
|
||||||
...customOpts
|
}
|
||||||
}
|
|
||||||
note_create.createNote(notePath, opts).then(({ branch }) => {
|
const noteUrl = customOpts.parentNoteLink ?? parentNotePath;
|
||||||
|
if (noteUrl) {
|
||||||
|
customOpts.parentNoteLink = noteUrl;
|
||||||
|
customOpts.activate = false;
|
||||||
|
note_create.createNote(customOpts).then(({ branch }) => {
|
||||||
if (branch) {
|
if (branch) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!api.current) return;
|
if (!api.current) return;
|
||||||
@ -27,6 +33,7 @@ export default function useRowTableEditing(api: RefObject<Tabulator>, attributeD
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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, { SuggestionMode, Suggestion } from "../../services/note_autocomplete";
|
||||||
import { logError } from "../../services/ws";
|
import { logError } from "../../services/ws";
|
||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import { refToJQuerySelector } from "../react/react_utils";
|
import { refToJQuerySelector } from "../react/react_utils";
|
||||||
@ -58,7 +58,7 @@ export default function AddLinkDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (suggestion.notePath) {
|
if (suggestion.notePath) {
|
||||||
const noteId = tree.getNoteIdFromUrl(suggestion.notePath);
|
const noteId = tree.getNoteIdFromLink(suggestion.notePath);
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
setDefaultLinkTitle(noteId);
|
setDefaultLinkTitle(noteId);
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ export default function AddLinkDialog() {
|
|||||||
onChange={setSuggestion}
|
onChange={setSuggestion}
|
||||||
opts={{
|
opts={{
|
||||||
allowExternalLinks: true,
|
allowExternalLinks: true,
|
||||||
allowCreatingNotes: true
|
suggestionMode: SuggestionMode.SuggestCreateAndLink,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</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 { SuggestionMode, Suggestion, triggerRecentNotes } from "../../services/note_autocomplete.js";
|
||||||
import tree from "../../services/tree";
|
import tree from "../../services/tree";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
@ -50,7 +50,7 @@ export default function IncludeNoteDialog() {
|
|||||||
inputRef={autoCompleteRef}
|
inputRef={autoCompleteRef}
|
||||||
opts={{
|
opts={{
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowCreatingNotes: true
|
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@ -71,7 +71,7 @@ export default function IncludeNoteDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function includeNote(notePath: string, editorApi: CKEditorApi, boxSize: BoxSize) {
|
async function includeNote(notePath: string, editorApi: CKEditorApi, boxSize: BoxSize) {
|
||||||
const noteId = tree.getNoteIdFromUrl(notePath);
|
const noteId = tree.getNoteIdFromLink(notePath);
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
|
||||||
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,53 @@ 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;
|
||||||
|
|
||||||
|
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,14 +70,14 @@ 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) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setShown(false);
|
setShown(false);
|
||||||
if (suggestion.notePath) {
|
if (suggestion.notePath) {
|
||||||
appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath);
|
appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath);
|
||||||
@ -70,12 +89,12 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
function onShown() {
|
function onShown() {
|
||||||
const $autoComplete = refToJQuerySelector(autocompleteRef);
|
const $autoComplete = refToJQuerySelector(autocompleteRef);
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "last-search":
|
case Mode.LastSearch:
|
||||||
break;
|
break;
|
||||||
case "recent-notes":
|
case Mode.RecentNotes:
|
||||||
note_autocomplete.showRecentNotes($autoComplete);
|
note_autocomplete.showRecentNotes($autoComplete);
|
||||||
break;
|
break;
|
||||||
case "commands":
|
case Mode.Commands:
|
||||||
note_autocomplete.showAllCommands($autoComplete);
|
note_autocomplete.showAllCommands($autoComplete);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -83,7 +102,7 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
$autoComplete
|
$autoComplete
|
||||||
.trigger("focus")
|
.trigger("focus")
|
||||||
.trigger("select");
|
.trigger("select");
|
||||||
|
|
||||||
// Add keyboard shortcut for full search
|
// Add keyboard shortcut for full search
|
||||||
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
|
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
|
||||||
if (!isCommandMode) {
|
if (!isCommandMode) {
|
||||||
@ -91,7 +110,7 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showInFullSearch() {
|
async function showInFullSearch() {
|
||||||
try {
|
try {
|
||||||
setShown(false);
|
setShown(false);
|
||||||
@ -116,7 +135,7 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
container={containerRef}
|
container={containerRef}
|
||||||
text={initialText}
|
text={initialText}
|
||||||
opts={{
|
opts={{
|
||||||
allowCreatingNotes: true,
|
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowJumpToSearchNotes: true,
|
allowJumpToSearchNotes: true,
|
||||||
isCommandPalette: true
|
isCommandPalette: true
|
||||||
@ -129,9 +148,9 @@ export default function JumpToNoteDialogComponent() {
|
|||||||
/>}
|
/>}
|
||||||
onShown={onShown}
|
onShown={onShown}
|
||||||
onHidden={() => setShown(false)}
|
onHidden={() => setShown(false)}
|
||||||
footer={!isCommandMode && <Button
|
footer={!isCommandMode && <Button
|
||||||
className="show-in-full-text-button"
|
className="show-in-full-text-button"
|
||||||
text={t("jump_to_note.search_button")}
|
text={t("jump_to_note.search_button")}
|
||||||
keyboardShortcut="Ctrl+Enter"
|
keyboardShortcut="Ctrl+Enter"
|
||||||
onClick={showInFullSearch}
|
onClick={showInFullSearch}
|
||||||
/>}
|
/>}
|
||||||
|
|||||||
@ -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 { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
|
||||||
import Badge from "../react/Badge";
|
import Badge from "../react/Badge";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
|
||||||
@ -76,6 +76,7 @@ export default function NoteTypeChooserDialogComponent() {
|
|||||||
onHidden={() => {
|
onHidden={() => {
|
||||||
callback?.({ success: false });
|
callback?.({ success: false });
|
||||||
setShown(false);
|
setShown(false);
|
||||||
|
setParentNote(null);
|
||||||
}}
|
}}
|
||||||
show={shown}
|
show={shown}
|
||||||
stackable
|
stackable
|
||||||
@ -85,7 +86,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,
|
suggestionMode: SuggestionMode.SuggestNothing,
|
||||||
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, { SuggestionMode } 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, SuggestionMode.SuggestCreateAndLink),
|
||||||
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,14 @@ export default function MobileDetailMenu() {
|
|||||||
],
|
],
|
||||||
selectMenuItemHandler: async ({ command }) => {
|
selectMenuItemHandler: async ({ command }) => {
|
||||||
if (command === "insertChildNote") {
|
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") {
|
} else if (command === "delete") {
|
||||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
if (!notePath) {
|
if (!notePath) {
|
||||||
|
|||||||
@ -224,7 +224,13 @@ 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.createNote(
|
||||||
|
{
|
||||||
|
target: "into",
|
||||||
|
parentNoteLink: 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 });
|
||||||
@ -1403,10 +1409,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
let node: Fancytree.FancytreeNode | null | undefined = await this.expandToNote(activeNotePath, false);
|
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,
|
// 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
|
// 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) {
|
if (noteId) {
|
||||||
const notesById = this.getNodesByNoteId(noteId);
|
const notesById = this.getNodesByNoteId(noteId);
|
||||||
@ -1836,9 +1842,13 @@ 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.createNote(
|
||||||
isProtected: node.data.isProtected
|
{
|
||||||
});
|
target: "into",
|
||||||
|
parentNoteLink: notePath,
|
||||||
|
isProtected: node.data.isProtected
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new TouchBar.TouchBarButton({
|
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 { 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, { SuggestionMode, Suggestion } from "../../../services/note_autocomplete.js";
|
||||||
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 { CreateNoteAction } from "@triliumnext/commons";
|
||||||
|
|
||||||
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, SuggestionMode.SuggestCreateAndLink),
|
||||||
itemRenderer: (_item) => {
|
itemRenderer: (_item) => {
|
||||||
const item = _item as Suggestion;
|
const item = _item as Suggestion;
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
@ -247,16 +248,18 @@ 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: CreateNoteAction
|
||||||
activate: false,
|
): Promise<string> => {
|
||||||
title: title
|
const { note } = await note_create.createNoteFromAction(
|
||||||
});
|
action,
|
||||||
}
|
true,
|
||||||
|
title,
|
||||||
return result?.note?.getBestNotePathString();
|
parentNotePath,
|
||||||
|
);
|
||||||
|
return note?.getBestNotePathString() ?? "";
|
||||||
}
|
}
|
||||||
}), [ notePath ]));
|
}), [ notePath ]));
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import FormGroup from "../react/FormGroup";
|
|||||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||||
import "./Empty.css";
|
import "./Empty.css";
|
||||||
import { ParentComponent, refToJQuerySelector } from "../react/react_utils";
|
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 appContext from "../../components/app_context";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import search from "../../services/search";
|
import search from "../../services/search";
|
||||||
@ -38,7 +38,7 @@ function NoteSearch({ ntxId }: { ntxId: string | null }) {
|
|||||||
inputRef={autocompleteRef}
|
inputRef={autocompleteRef}
|
||||||
opts={{
|
opts={{
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowCreatingNotes: true,
|
suggestionMode: SuggestionMode.SuggestCreateOnly,
|
||||||
allowJumpToSearchNotes: true,
|
allowJumpToSearchNotes: true,
|
||||||
}}
|
}}
|
||||||
onChange={suggestion => {
|
onChange={suggestion => {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import note_create from "../../../services/note_create";
|
|||||||
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
|
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
import { buildSelectedBackgroundColor } from "../../../components/touch_bar";
|
import { buildSelectedBackgroundColor } from "../../../components/touch_bar";
|
||||||
import { deferred } from "@triliumnext/commons";
|
import { CreateNoteAction, deferred } from "@triliumnext/commons";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,17 +115,18 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
},
|
},
|
||||||
loadIncludedNote,
|
loadIncludedNote,
|
||||||
// Creating notes in @-completion
|
// Creating notes in @-completion
|
||||||
async createNoteForReferenceLink(title: string) {
|
async createNoteFromCkEditor (
|
||||||
const notePath = noteContext?.notePath;
|
title: string,
|
||||||
if (!notePath) return;
|
parentNotePath: string | undefined,
|
||||||
|
action: CreateNoteAction
|
||||||
const resp = await note_create.createNoteWithTypePrompt(notePath, {
|
): Promise<string> {
|
||||||
activate: false,
|
const { note }= await note_create.createNoteFromAction(
|
||||||
title: title
|
action,
|
||||||
});
|
true,
|
||||||
|
title,
|
||||||
if (!resp || !resp.note) return;
|
parentNotePath,
|
||||||
return resp.note.getBestNotePathString();
|
)
|
||||||
|
return note?.getBestNotePathString() ?? "";
|
||||||
},
|
},
|
||||||
// Keyboard shortcut
|
// Keyboard shortcut
|
||||||
async followLinkUnderCursorCommand() {
|
async followLinkUnderCursorCommand() {
|
||||||
@ -162,7 +163,9 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
|||||||
// 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 (noteContext && parentNotePath) {
|
if (noteContext && parentNotePath) {
|
||||||
note_create.createNote(parentNotePath, {
|
note_create.createNote({
|
||||||
|
parentNoteLink: parentNotePath,
|
||||||
|
target: "into",
|
||||||
isProtected: note.isProtected,
|
isProtected: note.isProtected,
|
||||||
saveSelection: true,
|
saveSelection: true,
|
||||||
textEditor: await noteContext?.getTextEditor()
|
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 { copyTextWithToast } from "../../../services/clipboard_ext.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, { SuggestionMode, 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, SuggestionMode.SuggestCreateAndLink),
|
||||||
itemRenderer: (item) => {
|
itemRenderer: (item) => {
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import path from "path";
|
import path, { join } from "path";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import { LOCALES } from "@triliumnext/commons";
|
import { LOCALES } from "@triliumnext/commons";
|
||||||
import { PRODUCT_NAME } from "../src/app-info.js";
|
import { PRODUCT_NAME } from "../src/app-info.js";
|
||||||
import type { ForgeConfig } from "@electron-forge/shared-types";
|
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
const ELECTRON_FORGE_DIR = __dirname;
|
const ELECTRON_FORGE_DIR = __dirname;
|
||||||
|
|
||||||
@ -228,8 +229,22 @@ const config: ForgeConfig = {
|
|||||||
// Ensure all locales that should be kept are actually present.
|
// Ensure all locales that should be kept are actually present.
|
||||||
for (const locale of localesToKeep) {
|
for (const locale of localesToKeep) {
|
||||||
if (!keptLocales.has(locale)) {
|
if (!keptLocales.has(locale)) {
|
||||||
console.error(`Locale ${locale} was not found in the packaged app.`);
|
throw new Error(`Locale ${locale} was not found in the packaged app.`);
|
||||||
process.exit(1);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
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;
|
export default config;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ interface GotoOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BASE_URL = "http://127.0.0.1:8082";
|
const BASE_URL = "http://127.0.0.1:8082";
|
||||||
|
const NUM_OF_CREATE_NOTE_OPTIONS = 2;
|
||||||
|
|
||||||
interface DropdownLocator extends Locator {
|
interface DropdownLocator extends Locator {
|
||||||
selectOptionByText: (text: string) => Promise<void>;
|
selectOptionByText: (text: string) => Promise<void>;
|
||||||
@ -73,7 +74,8 @@ export default class App {
|
|||||||
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
|
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
|
||||||
await expect(resultsSelector).toContainText(noteTitle);
|
await expect(resultsSelector).toContainText(noteTitle);
|
||||||
await resultsSelector.locator(".aa-suggestion", { hasText: 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();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import "ckeditor5";
|
import "ckeditor5";
|
||||||
|
import { type CreateNoteAction } from "@triliumnext/commons"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Component {
|
interface Component {
|
||||||
@ -7,7 +8,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: CreateNoteAction): 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 { CreateNoteAction } from "@triliumnext/commons"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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?: CreateNoteAction;
|
||||||
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 === CreateNoteAction.CreateNote ||
|
||||||
const component = glob.getComponentByEl<EditorComponent>(editorEl);
|
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 => {
|
// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,4 +10,5 @@ export * from "./lib/server_api.js";
|
|||||||
export * from "./lib/shared_constants.js";
|
export * from "./lib/shared_constants.js";
|
||||||
export * from "./lib/ws_api.js";
|
export * from "./lib/ws_api.js";
|
||||||
export * from "./lib/attribute_names.js";
|
export * from "./lib/attribute_names.js";
|
||||||
|
export * from "./lib/create_note_actions.js";
|
||||||
export * from "./lib/utils.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